Compare commits

...

1402 Commits
0.1.1 ... 0.3.1

Author SHA1 Message Date
ibuler
edad26e05b Merge pull request #183 from jumpserver/connect_slow
fix(connect) 修复paramiko连接速度特么慢问题
2016-03-31 00:16:18 +08:00
ibuler
6c8117045f fix(connect) 修复paramiko连接速度特么慢问题
导致的原因是 pycrypto库有问题

fixed
2016-03-31 00:14:46 +08:00
ibuler
b573170d69 Merge pull request #182 from jumpserver/web_width
fix(web terminal) 修改web terminal初始窗口大小
2016-03-30 22:25:35 +08:00
ibuler
1d14f08541 fix(web terminal) 修改web terminal初始窗口大小
代码缺陷,没有复用
2016-03-30 22:22:43 +08:00
ibuler
489d796d7b Merge pull request #180 from jumpserver/bugfix
修复日志回放问题
2016-03-29 23:52:19 +08:00
liuzheng712
1efede4de8 bugfix 2016-03-29 21:37:56 +08:00
liuzheng712
a1187757bc bugfix 2016-03-29 21:22:18 +08:00
liuzheng712
263ff1ee08 bugfix 2016-03-29 21:18:04 +08:00
liuzheng712
2f2289a863 bugfix 2016-03-29 21:16:30 +08:00
liuzheng712
e07305ef46 bugfix 2016-03-29 21:06:46 +08:00
liuzheng712
1c61ed6a8b bugfix 2016-03-29 21:05:29 +08:00
liuzheng712
cf1da2a420 bugfix 2016-03-29 20:56:19 +08:00
liuzheng712
7de03c0fc7 Merge branch 'master' of github.com:jumpserver/jumpserver 2016-03-29 20:44:45 +08:00
liuzheng712
9c38f39e5b 紧急修复监控白屏问题 2016-03-29 17:20:05 +08:00
ibuler
0937e64c2e Merge pull request #178 from jumpserver/dev
Fix a bug for get_log function 

hot fix
2016-03-29 17:18:46 +08:00
ibuler
5a6e0283dd Merge branch 'master' into dev 2016-03-29 17:17:54 +08:00
ibuler
5489797900 Merge branch 'dev' 2016-03-29 17:17:03 +08:00
liuzheng712
6e48fe6357 紧急修复监控白屏问题 2016-03-29 17:16:26 +08:00
ibuler
485b45675d Merge pull request #177 from jumpserver/fixed_log
fix(log) fix load_full_log function bug
2016-03-29 17:14:15 +08:00
ibuler
d31f882f1a fix(log) fix load_full_log function bug
fixed
2016-03-29 17:11:56 +08:00
ibuler
209c078614 Merge pull request #176 from jumpserver/merge_master
Merge with master
2016-03-29 15:39:46 +08:00
ibuler
d2b4594bc2 Merge branch 'master' into dev 2016-03-29 15:38:28 +08:00
ibuler
6fcfb385cd Update README.md 2016-03-29 14:07:44 +08:00
ibuler
b12b83cbb5 Update requirements.txt 2016-03-29 14:07:14 +08:00
ibuler
26804b4093 Merge pull request #174 from jumpserver/dev
Release 0.3.1 version, Fix most bugs
2016-03-29 11:33:05 +08:00
ibuler
8ebcd47599 Merge pull request #173 from jumpserver/version_num
change(version num) change jumpserver version
2016-03-28 23:38:42 +08:00
ibuler
d810a082c5 change(version num) change jumpserver version
from 0.3.0 -> 0.3.1
2016-03-28 23:37:32 +08:00
ibuler
2f9255a46f Merge pull request #172 from jumpserver/web_log
fix(web terminal and log kill) fix close web terminal when not init finished
2016-03-28 23:09:37 +08:00
ibuler
5d0171d5b7 fix(web terminal and log kill) fix close web terminal when not init finished
when web terminal not init complete, you close the window, online log you will see the log and cann't kill it

catch a except fix it
2016-03-28 23:04:30 +08:00
ibuler
c5382c88a2 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2016-03-28 22:59:35 +08:00
ibuler
98f9e632ac Merge pull request #170 from jumpserver/TermLogRecorder
Term log recorder, 需要数据库增加filename字段
2016-03-28 22:59:06 +08:00
ibuler
f912869de6 fix(web monitor) 修改监控特殊编码卡住
编码字符,强制utf-8
2016-03-28 22:00:48 +08:00
liuzheng712
8cd6d23ff2 update 2016-03-28 20:24:02 +08:00
liuzheng712
f3d863ea45 filename 2016-03-28 20:19:49 +08:00
liuzheng712
958909551d bug_fix 2016-03-28 20:09:51 +08:00
liuzheng712
b132a0c0b5 use_old_way 2016-03-28 20:06:05 +08:00
ibuler
6e7e4d4742 Merge pull request #169 from jumpserver/new-line-
New line
2016-03-28 19:16:48 +08:00
ibuler
c95c76d0e0 change(web terminal) 修复web terminal遇到-换行
添加css

	white-space: nowrap;
        display: inline-block;

之前通过修改字体解决
2016-03-28 19:14:45 +08:00
ibuler
8735af4cbb fix(monitor) 修复监控-换行
修复实时监控-导致的换行,添加css

  .terminal {
      white-space: nowrap;
      display: inline-block;
  }
2016-03-28 19:11:56 +08:00
liuzheng712
edfef5822f bug fix 2016-03-27 22:58:59 +08:00
liuzheng712
ebef54ca44 Merge branch 'TermLogRecorder' of github.com:jumpserver/jumpserver into TermLogRecorder 2016-03-27 22:30:04 +08:00
liuzheng712
c937abe098 bugfix 2016-03-27 22:29:39 +08:00
liuzheng712
587c9b2c3f bugfix 2016-03-27 22:20:20 +08:00
kelianchun
74ac1dc06b Merge pull request #167 from kelianchun/TermLogRecorder
connect.py use TermLog
2016-03-27 22:00:59 +08:00
kelianchun_miller
0707b68796 connect.py use TermLog 2016-03-27 22:00:00 +08:00
liuzheng712
445a83f78a remove this log in loglist 2016-03-27 12:17:02 +08:00
liuzheng712
bc1d89da89 here I didn't use int to record the dict , changed 2016-03-27 11:13:15 +08:00
liuzheng712
dc5751951e disuse pyinotify, use TermLogRecord to record the log and monitor 2016-03-27 11:11:50 +08:00
ibuler
3ca6629175 Merge pull request #166 from jumpserver/font_size
change(font) 修改使用websocket的页面字体,增加监控size
2016-03-26 11:42:12 +08:00
ibuler
1e1aa67b3e change(font) 修改使用websocket的页面字体,增加监控size
增加 "Microsoft Yahei"
2016-03-26 11:39:49 +08:00
kelianchun
3dfd9cd512 Merge pull request #165 from kelianchun/dev
pull issue #120  remove ps1
2016-03-26 08:50:21 +08:00
kelianchun_miller
6a51bd1a1c pull issue #120 remove ps1 2016-03-26 00:01:48 +08:00
liuzheng712
5e329f51a4 Merge branch 'dev' of github.com:jumpserver/jumpserver into TermLogRecorder 2016-03-25 23:58:04 +08:00
kelianchun
d86cebf99a Merge pull request #164 from kelianchun/dev
pull issue #120  remove ps1
2016-03-25 23:55:26 +08:00
liuzheng712
9b43c6c238 update 2016-03-25 23:52:48 +08:00
liuzheng712
1084be4712 update 2016-03-25 22:28:49 +08:00
liuzheng712
44b2bcb759 修改日志数据库部分 2016-03-25 22:24:56 +08:00
liuzheng712
2d1e001ddf 日志回放ok 2016-03-25 22:20:50 +08:00
liuzheng712
8e8d8c9d6a 日志回放OK 2016-03-25 22:19:31 +08:00
liuzheng712
a1862d912a Merge branch 'dev' of github.com:jumpserver/jumpserver into TermLogRecorder 2016-03-25 21:08:55 +08:00
kelianchun_miller
fd713e0e5c pull issue #120 remove ps1 2016-03-25 21:08:52 +08:00
ibuler
d7442b4879 Update views.py 2016-03-25 17:36:12 +08:00
ibuler
4f79e909e5 Merge pull request #161 from jumpserver/user_edit
fix(user edit) 修改用户导致密码问题
2016-03-25 17:20:21 +08:00
ibuler
69061791ed fix(user edit) 修改用户导致密码问题
简单修改,更改结构和变量名

close #160
2016-03-25 17:18:41 +08:00
ibuler
38f15c976f Merge pull request #159 from jumpserver/terminal_font
fix(web terminal) Windows 浏览器使用chrome -换行
2016-03-24 19:07:55 +08:00
ibuler
b745ebdda1 fix(web terminal) Windows 浏览器使用chrome -换行
经测试由于字体原因,可以安装Monaco字体解决,然而不太方便

添加第二字体为 微软雅黑,经测试解决

fixed
close #158
2016-03-24 19:03:20 +08:00
ibuler
ae58fd548f Merge pull request #157 from jumpserver/force_del_user
fix(userdel) 修复无法删除在线用户bug
2016-03-24 18:07:28 +08:00
ibuler
fbc3078c07 fix(userdel) 修复无法删除在线用户bug
当用户已经登录到jumpserver时,web上删除用户时失败,也没有提示

修改方法: userdel -r -f 强制删除

close #156
2016-03-24 18:04:04 +08:00
liuzheng712
860c7f1508 Merge branch 'dev' of github.com:jumpserver/jumpserver into TermLogRecorder 2016-03-24 17:29:59 +08:00
liuzheng712
3fcd9589a4 update 2016-03-24 17:29:50 +08:00
liuzheng712
ba5e90abc3 update 2016-03-24 17:17:47 +08:00
kelianchun
5587ff59b0 Merge pull request #155 from kelianchun/dev
Dev
2016-03-24 13:35:46 +08:00
kelianchun
6d329b130a remove ps1 search
remove ps1 search
2016-03-24 13:02:02 +08:00
liuzheng712
357aea1693 update jumpserver.conf only for myself 2016-03-24 12:49:39 +08:00
liuzheng712
3ab0c94496 update settings for myself, use sqlite 2016-03-24 12:49:06 +08:00
liuzheng712
b3f83c3362 update 2016-03-23 22:38:02 +08:00
liuzheng712
17a7470a2a update_TermLogRecorder 2016-03-23 18:26:23 +08:00
liuzheng712
365a5ccd46 update_TermLogRecorder 2016-03-23 18:25:18 +08:00
liuzheng712
742a2b1b85 Merge branch 'dev' of github.com:jumpserver/jumpserver into TermLogRecorder 2016-03-23 17:08:33 +08:00
liuzheng712
388ebe3bee update 2016-03-23 17:07:57 +08:00
ibuler
6d302eb25a Merge pull request #154 from jumpserver/term_log_bug
fix: command recoder bug fix
2016-03-23 17:07:28 +08:00
liuzheng712
fa1f2404d9 日志记录 2016-03-23 17:05:35 +08:00
liuzheng712
52d8825d95 修复tmux等会重复出现命令的bug 2016-03-23 17:05:02 +08:00
liuzheng712
e743ae3dbe model 2016-03-23 16:36:55 +08:00
kelianchun
db72048c31 Update run_server.py 2016-03-23 15:47:08 +08:00
liuzheng712
f1ff52eb6b fix: command recoder bug fix 2016-03-23 15:46:42 +08:00
ibuler
0b05899818 Merge pull request #152 from jumpserver/pyte
change(install.py) 增加pyte库
2016-03-23 15:12:07 +08:00
ibuler
bd59b6316e change(install.py) 增加pyte库
增加pyte库来分析命令
2016-03-23 15:10:28 +08:00
jiaxiangkong
2c74c8a8df Update README.md 2016-03-23 14:42:26 +08:00
kelianchun
dc2ea0d6bf Merge pull request #149 from kelianchun/dev
Dev
2016-03-23 12:40:06 +08:00
kelianchun
73382cbcfb Update run_server.py 2016-03-23 12:37:55 +08:00
kelianchun
e6254ddc2c deal command data
deal command data
2016-03-23 12:36:29 +08:00
kelianchun
30607730f2 Merge pull request #148 from kelianchun/dev
no command data
2016-03-23 12:29:38 +08:00
kelianchun
b92c5188d1 no command data
no command data
2016-03-23 12:28:32 +08:00
ibuler
524cd1d990 Merge pull request #147 from jumpserver/bug_asset_upload_01
fix asset upload bug
2016-03-23 12:09:20 +08:00
wangyong
231f961945 fix asset upload bug 2016-03-23 11:56:32 +08:00
ibuler
267c1cd696 Merge pull request #142 from jumpserver/bug_asset_upload
fix asset upload bug
2016-03-22 18:03:00 +08:00
wangyong
a1e947ae1e fix asset upload bug 2016-03-22 17:32:43 +08:00
ibuler
300e106143 Merge pull request #140 from Astraeux/smtp_ssl_fix
SMTP发邮件支持SSL
2016-03-22 16:32:51 +08:00
ibuler
57e85affde Merge pull request #141 from jumpserver/termShake
fix: when moniter the terminal, fix the shake
2016-03-22 10:22:42 +08:00
liuzheng712
34b74b9e4e fix: when moniter the terminal, fix the shake 2016-03-21 22:51:28 +08:00
Astraeux
c218949556 SMTP发邮件支持SSL 2016-03-21 21:55:03 +08:00
Astraeux
4b90cd9b82 Merge pull request #3 from jumpserver/dev
update from origin
2016-03-21 20:08:34 +08:00
ibuler
fcb3fd7186 Merge pull request #139 from jumpserver/issue_52_bugfix
Issue 52 bugfix: 修复录像播放,web terminal hang住bug
2016-03-21 11:11:21 +08:00
ibuler
4dbc6803fb Update README.md 2016-03-21 11:00:31 +08:00
ibuler
b6af8368b6 Merge pull request #138 from jumpserver/role_pass_long_bug
修复系统用户role密码过长引起的异常
2016-03-21 10:40:59 +08:00
ibuler
6e4b291808 fix(perm_role_edit) 修复编辑系统用户密码过长bug
1. 添加前端验证

close #173
2016-03-21 10:32:08 +08:00
ibuler
4c88ea3c05 fix(perm_role_add) 修复添加系统用户密码过长导致的异常bug
1. 修改表结构password长度
2. 修改template添加js验证
2016-03-21 10:30:29 +08:00
ibuler
0e5fd68e6c Merge pull request #129 from jumpserver/bug_fix_100_and_127
fix (jperm):   统一调整系统用户 仅使用秘钥进行通信, 已存在的用户不会修改密码
2016-03-19 13:18:06 +08:00
liuzheng712
1103ee8f52 len(vim_data) 2016-03-18 23:55:06 +08:00
liuzheng712
9efa9e979c len(vim_data) 2016-03-18 23:51:25 +08:00
liuzheng712
f331b4471f Merge branch 'dev' of github.com:jumpserver/jumpserver into issue_52_bugfix 2016-03-18 23:41:19 +08:00
kelianchun
f11016f57c Merge pull request #136 from kelianchun/dev
pull issue #120
2016-03-18 23:38:26 +08:00
Astraeux
4b53bae57b Merge pull request #1 from jumpserver/dev
update from origin
2016-03-18 23:37:22 +08:00
kelianchun_miller
b2ccbc3f9e pull issue #120 2016-03-18 23:36:31 +08:00
kelianchun_miller
7dcf050dc4 pull issue #120 2016-03-18 22:06:40 +08:00
kelianchun_miller
717869fd30 Merge remote-tracking branch 'remotes/origin/master' into dev 2016-03-18 22:04:12 +08:00
kelianchun_miller
598d6cffa4 pull issue #120 2016-03-18 20:43:25 +08:00
liuzheng712
c022c81100 Merge branch 'dev' of github.com:jumpserver/jumpserver into issue_52_bugfix 2016-03-18 17:32:46 +08:00
liuzheng712
5af9dd655e delete_require_chardet 2016-03-18 17:08:15 +08:00
ibuler
abdf9c72ca Merge pull request #122 from jumpserver/bug_fix_80
fix (jasset):  修复连接超时,以及freebsd无法更新硬件信息
2016-03-18 17:00:09 +08:00
ibuler
3778724b73 Merge pull request #134 from kelianchun/dev
修复命令截取bug
2016-03-18 16:46:02 +08:00
kelianchun
d76865c1ed pull issue #120
修复了命令处理的bug
2016-03-18 15:38:31 +08:00
liuzheng712
3ccf7adac1 终于修复了乱码啦啦啦 2016-03-18 15:01:07 +08:00
yumaojun
c0e8ff8620 fix (jperm): 统一调整系统用户 仅使用秘钥进行通信, 已存在的用户不会修改密码
1. perm_role_add 同上 保留 密码选项
2. perm_role_edit 同上 保留 密码选项
3. 仅保持入数据,不设计远程操作

close #100
close #127
2016-03-16 23:03:43 +08:00
yumaojun
ca75484eb8 fix (jperm): 统一调整系统用户 仅使用秘钥进行通信, 已存在的用户不会修改密码
1. perm_role_push 模板取消密码选项,但是为了 那么留下秘钥推送可勾选,允许空推送
2. perm_role_add 同上 取消 密码选项
3. perm_role_edit 同上 取消 密码选项
4. views 调整role push add edit 相关视图
5. perm_api,调整 gen_resource,仅支持秘钥认证(是否需要支持密码认证,我觉得没必要,如果需要请提出)

close #100
close #127
2016-03-16 22:30:39 +08:00
liuzheng712
8adf1c5d71 requirement 2016-03-15 15:11:33 +08:00
liuzheng712
5f35b740df test 2016-03-15 15:06:37 +08:00
liuzheng712
b0d1dd9485 test 2016-03-15 15:04:15 +08:00
liuzheng712
ade8fb927c test 2016-03-15 15:02:16 +08:00
liuzheng712
31708c0d24 udpate 2016-03-15 14:53:30 +08:00
liuzheng712
9b3a51469e install_chardet 2016-03-15 14:46:25 +08:00
yumaojun
cdd31227b7 fix (jasset):  修复连接超时,以及freebsd无法更新硬件信息
1. 超时原因应该是sshd 的配置引起的,当前bsd的更新 相对于其他发行版 慢,这个原因估计的bsd自身有关
 2. ansible 的setup模块对于b sd获取的字段 越有偏差,而且无法获取硬盘信息,这个后期可以 提交patch 给ansible的setup模块。

 close #80
2016-03-14 22:47:33 +08:00
jiaxiangkong
b02a45ee6c Update README.md 2016-03-10 17:43:37 +08:00
jiaxiangkong
f796e8f673 Update README.md 2016-03-10 17:42:45 +08:00
jiaxiangkong
930c57398a Update README.md 2016-03-10 17:40:54 +08:00
ibuler
6b39c9946b Merge pull request #116 from jumpserver/bug_fix_guang
fix(bug) 修复首页点击头像404问题
2016-03-06 09:51:31 +08:00
ibuler
3a71d7f1a8 fix(bug) 修复首页点击头像404问题
处理结果,直接干掉连接
2016-03-06 09:50:01 +08:00
liuzheng712
d48020a919 test 2016-03-04 23:59:18 +08:00
liuzheng712
210b3c8588 test 2016-03-04 23:57:39 +08:00
liuzheng712
bdf6d97478 test 2016-03-04 23:50:19 +08:00
liuzheng712
1ac9bebb8d fix: delete monkey patch 2016-03-04 23:17:19 +08:00
liuzheng712
ef3156e6f7 fix(test): monkey patch 2016-03-04 23:15:39 +08:00
liuzheng712
95a7557acf fix: 2016-03-04 20:42:08 +08:00
liuzheng712
8e30ffb840 fix(run_server.py): test if it invoke_shell bug 2016-03-04 20:40:37 +08:00
liuzheng712
20b5facc38 fix(webterminal): data 2016-03-04 20:30:58 +08:00
liuzheng712
f7eda41a54 fix(webterminal): data.data.data 2016-03-04 20:29:54 +08:00
liuzheng712
6a0edb63f0 fix(webterminal): data.data 2016-03-04 20:28:52 +08:00
liuzheng712
e7976e4235 fix(webterminal.js): }) bug 2016-03-04 20:22:46 +08:00
liuzheng712
2bd3fdb4e7 fix(webterminal): url bug 2016-03-04 20:21:20 +08:00
liuzheng712
94a4d68be2 fix(webterminal.js): send bug 2016-03-04 20:12:09 +08:00
liuzheng712
b879e724e2 fix(webterminal.js): forget rowHeight and colWidth 2016-03-04 20:09:05 +08:00
liuzheng712
234f79857c fix(webterminal): change it to HTML5 websocket 2016-03-04 20:07:05 +08:00
liuzheng712
f73d34b9cc fix(monitor): change monitor url 2016-03-04 19:48:52 +08:00
liuzheng712
936faad1b9 fix(base.jinja2): change script with term.js 2016-03-04 19:35:53 +08:00
ibuler
649783d2c3 Merge pull request #114 from jumpserver/bug_fix_guang
change(with pre fix) 修改安装和jumpserver.conf
2016-03-03 18:18:36 +08:00
ibuler
c0bacb7c7c change(with pre fix) 修改安装和jumpserver.conf
上次修改已可以自动获取 ws地址,去掉无用设置和配置文件指定
去掉install.py中的设置
2016-03-03 18:16:11 +08:00
ibuler
6436fab837 Merge pull request #113 from jumpserver/bug_fix_guang
fix(ws protocal): 修复所有ws自动获取
2016-03-03 18:09:26 +08:00
ibuler
e38620a2a2 Merge pull request #111 from jumpserver/install_bug_fix
修复一些install.py 中的小问题
2016-03-03 18:08:04 +08:00
ibuler
702b1bd3b7 fix(ws protocal): 修复所有ws自动获取
自动获取websocket无需再手动指定
2016-03-03 18:05:10 +08:00
ibuler
c2c833f279 Merge pull request #112 from jumpserver/issue_110_bugfix
修复webterminal vim后命令double
2016-03-03 17:21:42 +08:00
liuzheng712
a816a7b149 fix(webterminal): bug fix test 2016-03-03 17:03:47 +08:00
liuzheng712
32519f8eae fix: 2016-03-03 16:50:49 +08:00
liuzheng712
dbb4904513 fix(webterminal): test 2016-03-03 16:41:41 +08:00
liuzheng712
0e7a8c1a62 fix(webterminal): test 2016-03-03 16:39:14 +08:00
yumaojun
68582ca466 fix (install.py):  修复lockfile 创建失败问题 2016-03-03 16:37:12 +08:00
liuzheng712
b1f0297e82 fix(web terminal): test 2016-03-03 16:35:48 +08:00
liuzheng712
eeff2ab215 fix(webterminal): test 2016-03-03 16:34:22 +08:00
yumaojun
62fb6429f9 fix (install.py):  更新install.py 和 service.sh
1. 添加软件安装失败后的用户提醒
2. service.sh脚本添加对lockfile 目录缺失的处理。
2016-03-03 15:53:46 +08:00
ibuler
90c3ed96e3 Merge pull request #109 from jumpserver/bug_fix_guang
fix(web teminal): 修改动态改变窗口大小bug
2016-03-02 11:40:28 +08:00
ibuler
c79e625000 fix(web teminal): 修改动态改变窗口大小bug
变量名写错引起的不明显bug
2016-03-02 11:38:38 +08:00
Huang Chengwei
bce3def4c6 Update run_server.py
fix signal call in auth
2016-03-01 17:09:27 +08:00
ibuler
1437140f87 Merge pull request #108 from jumpserver/bug_fix_guang
fix(run_server.py) 修改一个常规错误,函数参数个数
2016-03-01 17:03:33 +08:00
ibuler
fd19bb6299 fix(run_server.py) 修改一个常规错误,函数参数个数
已fix
2016-03-01 17:01:11 +08:00
ibuler
a401440004 Merge pull request #105 from jumpserver/hot_fix_log
hot_fix(kill invalid connection) 紧急修复超时异常连接

修改日志参数
修改处理间隔,每10分钟处理一次
修改处理策略 ssh: 1小时不操作,就kill掉 web: 超过1天,就设置完成
2016-02-29 18:18:57 +08:00
ibuler
06aef8fb1a Merge pull request #106 from jumpserver/issue_57_bug_fix
1. 修复tonrado数据库连接bug
2. 修改定期处理长时间连接
3. 添加nginx文档

ref #57
2016-02-29 18:18:10 +08:00
ibuler
c418f142ba Merge remote-tracking branch 'origin/issue_57_bug_fix' into dev 2016-02-29 18:15:31 +08:00
ibuler
88d21caa19 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2016-02-29 18:14:50 +08:00
ibuler
f6c58054a6 Merge branch 'hot_fix_log' into dev 2016-02-29 17:59:25 +08:00
ibuler
b4d74bc589 hot_fix(kill invalid connection) 紧急修复超时异常连接
1. 修改日志参数
2. 修改处理间隔,每10分钟处理一次
3. 修改处理策略
   ssh: 1小时不操作,就kill掉
   web: 超过1天,就设置完成
2016-02-29 17:53:15 +08:00
ibuler
d7bb4c1005 fix(kill invalid connection) 紧急处理超时连接
1. 紧急修复异常bug
2. 修改处理策略
3. 每10分钟处理一次
   ssh连接:超过1小时没有操作就干掉
   web:超过1天的就设置完成
2016-02-29 17:48:14 +08:00
ibuler
3c610668b5 fix(invalid connection again) 定期处理长时间连接
1. 减少了处理时间间隔
2. 处理策略更改为 超过 1小时没有动的连接就干掉
2016-02-29 17:07:53 +08:00
ibuler
5d28a6e402 fix(invalid connection) 定期处理长时间连接
1. 减少了处理时间间隔
2. 处理策略更改为 超过 1小时没有动的连接就干掉
2016-02-29 16:35:09 +08:00
黄成维
e886b55727 fix signal send 2016-02-29 16:09:01 +08:00
黄成维
8c552ccc45 fix signal send 2016-02-29 16:06:20 +08:00
黄成维
054edeefb6 try fix tornado connection timeout 2016-02-29 15:03:48 +08:00
ibuler
6c599e0127 Merge pull request #101 from jumpserver/bug_fix_yu
install.py安装兼容多个平台
2016-02-29 12:21:41 +08:00
ibuler
4f97987061 Merge pull request #97 from jumpserver/bug_fix_guang
fix 启动脚本,配置文件,批量命令异常等
2016-02-29 12:17:31 +08:00
yumaojun
50e82c5b99 fix (install.py):  更新wiki地址 2016-02-29 11:08:48 +08:00
yumaojun
1f9337feca fix (install.py):  compatible fedora
1. mysql-python编译不过, 添加rpm-build 依赖
2016-02-29 10:58:31 +08:00
ibuler
cba53bba55 modify(jumpserver.conf) 修改jumpserver.conf默认配置
django和tornado统一入口后,配置默认启动端口80
websocket和web都使用该端口
2016-02-29 10:25:59 +08:00
yumaojun
427fda1015 fix (install.py):  compatible fedora
1. compatible fedora (test fedora22)
2016-02-29 10:09:05 +08:00
yumaojun
a95e1bb835 fix (install.py):  compatible fedora
1. compatible fedora (test fedora22)
2016-02-29 09:52:10 +08:00
Tad Wang
5ab882ae66 update configuration doc
typo of KEYWORD of SSL configuration
2016-02-29 01:37:20 +08:00
Tad Wang
e5ce5766be add document for nginx ssl setting #88
add document for nginx ssl configuration. #88
2016-02-29 01:27:51 +08:00
ibuler
725405e4fd change(install) 修改提示,更改变化的文件名 run_server.py
websocket -> websocket_url
2016-02-28 16:41:44 +08:00
ibuler
cad7a193b3 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2016-02-28 16:32:33 +08:00
ibuler
69ac8ae147 Merge pull request #99 from jumpserver/misunderstand_4_default_DB_user
安装脚本默认数据库用户名
2016-02-28 16:09:31 +08:00
yumaojun
247e5e7f24 fix (install.py):  little update to contain liuzheng pr.
1. 请输入数据库服务器用户 [root] , 修改成 [jumpserver]
2016-02-28 15:55:21 +08:00
yumaojun
04821a00f8 fix (install.py):  compatable centos7
1. use systemctl  stop firewalld
2. add  dependence:  readline-devel and lrzsz
3. use mariadb as mysql server
2016-02-28 15:47:39 +08:00
yumaojun
ad3178fe94 fix (install.py):  compatable centos7
1. use systemctl  stop firewalld
2. add  dependence:  readline-devel and lrzsz
2016-02-28 15:23:14 +08:00
yumaojun
ccd1a10892 fix (install.py):  compatable centos7
1. use systemctl  stop firewalld
2. add  dependence:  readline-devel and lrzsz
2016-02-28 15:01:55 +08:00
yumaojun
b80ad40f54 fix (install.py):  compatable centos7
1. use systemctl  stop firewalld
2. add  dependence:  readline-devel and lrzsz
2016-02-28 14:40:17 +08:00
liuzheng712
7cf190e3f9 refactor(install/install.py): default database user should be jumpserver 2016-02-27 21:25:09 +08:00
yumaojun
cac94245ea fix (install.py):  ubuntu auto install mysql-server
1. set ansible_api connector as paramiko
2. set ubuntu apt-get --force-yes  when install packages
2016-02-27 18:35:00 +08:00
yumaojun
a729e54425 fix (install.py):  ubuntu auto install mysql-server
1. auto install mysql server
2016-02-27 13:24:19 +08:00
yumaojun
d4b57fc1b0 fix (install.py):  ubuntu auto install mysql-server
1. auto install mysql server
2016-02-27 12:43:59 +08:00
yumaojun
d63b5772e4 fix (install.py):  check platform
1. check platform , support CentOS, ReaHat, Fedora, Ubuntu, debian
2016-02-27 12:05:34 +08:00
yumaojun
37e0f80fb8 fix (install.py): fix install.py add user failed and service failed
1. use shlex.os.system  replace  subprocess.call
2. next.py use bash service start
2016-02-27 12:01:44 +08:00
ibuler
532fbbd4f1 change(service.sh, run_websocket) 修改启动脚本, rename run_websocket.py
1. 修改启动脚本,支持放到 init.d
2. rename run_websocket.py -> run_server.py 交互式启动
2016-02-27 01:26:36 +08:00
ibuler
16f5906979 Merge pull request #96 from jumpserver/ip_port_config
jumpserver.conf添加字段,tornado根据需求监听端口
2016-02-27 00:24:15 +08:00
ibuler
bf3fb24c3a Merge pull request #95 from jumpserver/IP_get_bug
修复使用反向代理还不使用反向代理获取 remote_ip异常wen问题
2016-02-27 00:23:00 +08:00
ibuler
a8eb9f3e79 fix(jlog, websocket url) 修复ws使用 ws协议还是wss协议
1. 不再需要浏览器判断什么协议,需要在jumpserver.conf注明

ref #63
2016-02-27 00:19:04 +08:00
ibuler
a88d8ca410 fix(upload, download, exec, gn) 修复上传下载文件名,上传目录层次,执行命令回车报错,g+不存在id报错
1. 上传下载文件名 改为 时间+随机4位字母数字
2. 命令回车判断,为空返回
3. g+1判断,不过不存在该组,则返回

close #52
close #93

ref https://github.com/jumpserver/jumpserver/issues/53
ref https://github.com/jumpserver/jumpserver/issues/93
2016-02-26 23:51:21 +08:00
liuzheng712
ba8d808cd9 fix(service.sh): annotate django runserver 2016-02-26 23:09:19 +08:00
liuzheng712
d67aab573d fix: ip and port use jumpserver.conf to configure 2016-02-26 22:59:17 +08:00
liuzheng712
5098b1c696 fix(run_websocket.py): remote IP get bug
when use nginx  self.request.headers.get(X-Real-IP) will get real remote IP,
self.request.remote_ip will get 127.0.0.1 ; if not it will git null, so need to use
self.request.remote_ip for get the ip
2016-02-26 22:48:07 +08:00
yumaojun
0b9b94bc0b Merge branch 'bug_fix_yu' into dev
# Conflicts:
#	service.sh
2016-02-26 15:36:42 +08:00
yumaojun
8aec0c1ac7 fix (install jumpserver): install jumpserver compatible with ubuntu
1.  install.py  add  platform judge, fix get_ip_addr function
2.  next.py  little adjust
3. service.sh little adjust
2016-02-26 14:13:21 +08:00
liuzheng712
77f69fbc5e fix(service.sh): annotate django runserver 2016-02-26 11:52:53 +08:00
liuzheng712
14a0c1871f Merge branch 'issue_59_bug_fix' into dev 2016-02-26 10:53:29 +08:00
liuzheng712
49e7796df9 Merge branch 'feat_Only_run_one' into dev 2016-02-26 10:45:40 +08:00
liuzheng712
318a053dbf real ip 2016-02-26 10:38:04 +08:00
ibuler
29953eb7c7 Merge pull request #87 from liuzheng712/feat_run_one
整合Tornado和Django,修复web阻断功能
2016-02-26 10:12:23 +08:00
liuzheng712
1816723f16 fix(run_websocket.py): get real ip from headers 2016-02-25 22:43:52 +08:00
liuzheng712
5fb5c1bb40 merge 2016-02-25 21:16:09 +08:00
liuzheng712
4d5d56fe79 fix(jlog/views.py): kill bug, because cross domain 2016-02-25 21:07:18 +08:00
liuzheng712
7213c5c637 fix(log_online.html): log kill bug
change $.ajax to $.get
2016-02-25 21:04:27 +08:00
liuzheng712
eb161b978a test kill 2016-02-25 21:00:48 +08:00
liuzheng712
23d4c926b7 web log_kill 2016-02-25 20:45:48 +08:00
liuzheng712
c354b64679 web log_kill 2016-02-25 20:44:13 +08:00
liuzheng712
9c93d5a8d2 add /ws/ 2016-02-25 20:42:35 +08:00
liuzheng712
4f75f76db8 fix: web socket url update 2016-02-25 20:05:20 +08:00
ibuler
4dfef99216 Merge pull request #85 from jumpserver/bug_fix_issue_83
修复用户禁用后仍可ssh登陆跳板机
2016-02-25 18:42:59 +08:00
ibuler
47ce09393c fix(connect.py) 用户禁用后仍可ssh登陆jumpserver
修改connect.py添加判断
资产禁用还没有考虑,改动太大,留后续版本更改

ref https://github.com/jumpserver/jumpserver/issues/83
2016-02-25 18:34:46 +08:00
liuzheng712
92e9f988f3 feat(run_websocket.py): add main function, it need only run run_websocket.py
I want to run one program at once, open two terminal is ridiculous
2016-02-25 16:51:41 +08:00
liuzheng712
037a88f0f5 fix(term.js): my mistake
use yoshiokatsuneo term.js
2016-02-25 13:58:31 +08:00
yumaojun
e8db8addd7 fix (service.sh): ubuntu service.sh 脚本不可用
由于service.sh 脚本依赖外部的函数库 /etc/init.d/functions, 而ubuntu 和其他一起系统并没有这个文件,所以直接把 这个文件copy 到了当前目录下
不在依赖外部环境。
2016-02-25 13:26:18 +08:00
ibuler
d0ba0503e5 Merge pull request #81 from jumpserver/v3.0_beta_issue_62
V3.0 beta issue 62 用户下载key后,就删除,为了安全
2016-02-25 13:15:13 +08:00
liuzheng712
27be35ae77 fix(user_list.html & juser/views.py): only for user delete sshkey when downloaded, add generate butt
https://github.com/jumpserver/jumpserver/issues/62
2016-02-25 13:04:09 +08:00
yumaojun
835706f780 fix (jperm.view): 修复回收主机时, 未修改sudoers文件的bug
1. 恢复ansible 使用的 连接器配置未 smart
2. 修改perm_role_recycle 删除, 添加回收sudo配置.
2016-02-25 11:59:29 +08:00
ibuler
f79675b265 Merge pull request #78 from jumpserver/bug_fix_guang
#78 统一资产添加文案,修改添加用户流程,修复添加系统用户使用key推送,不生成密码
2016-02-24 15:06:20 +08:00
ibuler
0a35f757e9 modify(function arg) only for writing style
修改函数使用kwarg
2016-02-24 14:46:28 +08:00
liuzheng712
856852592d fix(term.js): CJK support, Copy and Paste support
Use yoshiokatsuneo's code, fix this bug. His idea is append a textarea and bind all click event on
it. That works for us

https://github.com/jumpserver/jumpserver/issues/59  https://github.com/chjj/term.js/pull/97
2016-02-24 13:39:00 +08:00
ibuler
f60a896926 modify(jasset) 统一管理用户文案
所有用得到管理用户文案的地,都进行了统一
更改了 管理用户使用默认的对齐
去掉了分隔线
2016-02-24 12:32:41 +08:00
ibuler
b6fc8b777f change(juse) 修改用户添加流程
1. 添加新用户,不在为该用户设置密码
2. 强制用户使用key登陆跳板机,为了安全性
3. 更改邮件文案和不发送邮件提示文案
2016-02-24 12:29:47 +08:00
ibuler
caefbdc917 fix(juser) 推送系统用户,选择密钥时不为用户生成密码
当推送系统用户时,选择系统用户使用密钥时没有必要为系统用户生成密码,
以免造成安全上的问题,在代码上也属于冗余.
2016-02-24 11:34:33 +08:00
yumaojun
c6823be302 修复sudo 命令 小写all 引起的推送失败(map) 2016-02-23 23:29:15 +08:00
yumaojun
7bfb1d19fe 修复sudo 命令 小写all 引起的推送失败 2016-02-23 23:04:46 +08:00
yumaojun
f24b34758c 修复sudo 命令 小写all 引起的推送失败 2016-02-23 21:14:54 +08:00
ibuler
d4b1bdef8e modify(with 0e9a962506) 变更启动脚本
1. 启动时添加 django crontab
2. 关闭时删除 django crontab
2016-02-23 18:46:00 +08:00
ibuler
dee1d31fc0 change(with 6be7003) 联动改变安装脚本
1. 为init.sh添加执行权限
2. 统一函数功能  修改其他需要改变权限的脚本或目录
2016-02-23 18:29:23 +08:00
ibuler
363bce82d8 Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2016-02-23 17:49:59 +08:00
ibuler
6be7003ac2 bugfix(登录初始化) 避免登录时ctrl+c进入系统内部
此bug信息见 #72
1. 修改思路 重命名zzjumpserver.sh,并移动到外层
2. 服务器添加用户时指定shell为 some_dir/jumpserver/init.sh

升级修复方案:
1. 删除/etc/profile.d/zzjumpserver.sh
2. git pull 更新
3. vim编辑 /etc/passwd,把之前建的用户的sh改为 some_dir/jumpserver/init.sh

终
Closes #72
closes #31
2016-02-23 17:40:33 +08:00
ibuler
586e43c8d0 Merge pull request #71 from jumpserver/dev
fix(bsd推送系统用户bug)
2016-02-23 15:50:59 +08:00
ibuler
18e66f52dd Merge remote-tracking branch 'origin/dev' into dev 2016-02-23 15:45:32 +08:00
yumaojun
09e86f0a6b Merge branch 'dev' of https://github.com/jumpserver/jumpserver into dev 2016-02-23 15:22:15 +08:00
yumaojun
3b5daf19c3 1. 修复freebsd 推送带sudo规则的用户时 , sudo 路径引发的问题。 2016-02-23 15:21:27 +08:00
liuzheng712
a037108cf3 refactor: double meaning 2016-02-23 14:23:07 +08:00
liuzheng712
b8a8c3ebf3 feat(juser/views.py;func:down_key): delete the private key when user download it
issue:62
2016-02-23 14:15:55 +08:00
ibuler
9fa7f8762e Merge branch 'master' into dev 2016-02-23 13:35:36 +08:00
ibuler
9ec457bf5f Merge branch 'dev' of github.com:jumpserver/jumpserver into dev 2016-02-23 13:35:28 +08:00
ibuler
2691cc0b6d Merge branch 'master' into dev 2016-02-23 13:33:32 +08:00
yumaojun
4645029a27 1. 配置ansible 使用paramiko进行链接(ssh 有卡死问题, 等待反馈.)
2. 修复freebsd 推送带sudo规则的用户时 由于 sed  引起的问题。
2016-02-23 11:48:09 +08:00
ibuler
b1768565c1 修复
1. 推送时 验证改为  /usr/sbin/visudo -c
    2. 添加系统用户的key 认证更改 支持 RSA|DSA
    3. web terminal 行数 -1
2016-02-22 16:31:33 +08:00
ibuler
7323b72c4b 修复
1. 推送时 验证改为  /usr/sbin/visudo -c
2. 添加系统用户的key 认证更改 支持 RSA|DSA
3. web terminal 行数 -1
2016-02-22 16:29:36 +08:00
ibuler
2e5c01a4da Merge pull request #47 from iambocai/master
当用户未被授予任何角色/主机权限时,提示用户
根据用户实际安装路径,替换启动脚本中connect.py的路径
修正几处拼写错误
2016-02-20 22:53:27 +08:00
ibuler
19dd1af4dc Merge pull request #49 from wangjunj/master
添加本地mysqld自启动服务
2016-02-20 22:51:15 +08:00
ibuler
0e9a962506 具体体现在 日志监控页,定期回收过期的在线log
需要运行python manage.py crontab add来添加

运行 python manage.py crontab remove 来去掉

crontab -l
2016-02-20 16:02:31 +08:00
iambocai bob.chen.cs@gmail.com
fa195c3808 update for pull#47 2016-02-17 15:16:36 +08:00
wangjunj
cd6cfc6ae9 Update install.py 2016-01-28 23:08:12 +08:00
wangjunj
02e9ba54f9 添加本地mysqld自启动服务
添加数据库自启动服务。修复服务器重启后./server.sh start 会提示错误——“Starting jumpsever
service:run_websocket.py not running”
2016-01-28 14:50:57 +08:00
ibuler
b79056295b Merge branch 'dev' 2016-01-26 15:37:27 +08:00
ibuler
8653630d83 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2016-01-26 15:37:19 +08:00
ibuler
99f09709ec 修改默认端口获取 2016-01-26 15:37:12 +08:00
wangyong
bcb914485d fix asset edit port not save 2016-01-26 15:35:47 +08:00
iambocai bob.chen.cs@gmail.com
0b79f754f9 1. 当用户未被授予任何角色/主机权限时,提示用户
2. 根据用户实际安装路径,替换启动脚本中connect.py的路径
3. 修正几处拼写错误
2016-01-25 18:21:07 +08:00
ibuler
fe87e32e2b roll back 2016-01-21 19:37:20 +08:00
ibuler
9c9390878e Merge branch 'master' of github.com:ibuler/jumpserver 2016-01-21 18:17:29 +08:00
ibuler
1c2cba64ec 修改推送默认端口bug 2016-01-21 18:17:18 +08:00
ibuler
b69853a608 Merge pull request #46 from wptad/master
监控支持 ssl
2016-01-21 13:21:59 +08:00
Tad Wang
852de35e3e fix term.js input error problem
handler is undefined.
2016-01-21 13:02:23 +08:00
Tad Wang
6fbb387488 fix wss support for web_monitor_uri
Https support
2016-01-21 11:53:56 +08:00
ibuler
36bfb50aad 系统用户支持. 2016-01-18 12:01:39 +08:00
ibuler
28e0ea3e81 支持用户名带小数点,上传文件大小最大2G 2016-01-15 15:54:49 +08:00
ibuler
6ba9191b30 update LANG setting 2016-01-14 11:58:53 +08:00
ibuler
b6f82ca020 修复vim修改窗口大小中断bug 2016-01-14 11:10:07 +08:00
ibuler
81a6f4841f 修复用户组编辑导致该用户组都丢失问题 2016-01-14 11:01:19 +08:00
ibuler
f2487a22cd 修复 su - 无法获取 Env的bug 2016-01-13 17:57:32 +08:00
ibuler
4d0331e105 Update connect.py 2016-01-13 15:51:29 +08:00
ibuler
66e53c2701 Update zzjumpserver.sh 2016-01-13 14:06:36 +08:00
ibuler
2e6f3d3579 Merge pull request #42 from hhding/master
Get SSH client IP address from environment
2016-01-13 13:20:28 +08:00
ibuler
80fde52dc5 Merge pull request #38 from wptad/master
fix wss issue when using https  #37 支持 wss 和 https
2016-01-13 12:32:16 +08:00
ibuler
82a88e0b0a fix 硬盘超过6块数据库溢出 2016-01-12 22:39:44 +08:00
wangyong
d3f9fc7a21 disk length 128 to 1024 2016-01-12 22:17:02 +08:00
ibuler
9d7c30336e 修复中文字符报错 2016-01-11 18:12:30 +08:00
ibuler
ff5b339ce8 Update asset_cu_list.html 2016-01-09 21:51:07 +08:00
ibuler
70c86d2a44 修改windows下 web terminal窗口高度 2016-01-09 21:50:22 +08:00
ibuler
0924484bc4 Update install.py 2016-01-09 21:42:44 +08:00
Honghui Ding
dd36857cf8 Get SSH client IP address 2016-01-09 07:37:28 +00:00
ibuler
2f54c369f7 Update README.md 2016-01-08 17:16:06 +08:00
ibuler
9e5402e2c3 Merge branch 'master' of github.com:ibuler/jumpserver 2016-01-08 11:49:33 +08:00
ibuler
097d77755a Merge branch 'dev' 2016-01-08 11:48:57 +08:00
wangyong
4fbeb5c172 fix cpu bug 2016-01-07 19:19:05 +08:00
ibuler
23b04fbbd7 Merge pull request #36 from chnliyong/master
Ubuntu的passwd命令不支持--stdin选项,使用chpasswd替代,在CentOS上已经验证有效
2016-01-07 19:06:12 +08:00
Tad Wang
d9455e3f9b fix wss issue when using https #37 2016-01-07 15:59:57 +08:00
yumaojun
beeb2442ad 回收sudo用户, 添加sudo别名添加规则检查 2016-01-07 15:21:39 +08:00
yumaojun
f6a228008b 回收sudo用户, 添加sudo别名添加规则检查 2016-01-07 15:15:44 +08:00
LI Yong
73522dc4c1 Ubuntu's passwd command doesn't support --stdin option, use chpasswd which is also available in CentOS 2016-01-07 14:09:11 +08:00
ibuler
d9b4f5504c Merge branch 'dev' 2016-01-05 19:29:56 +08:00
yumaojun
01511c0d5a Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2016-01-05 17:22:44 +08:00
yumaojun
01cf2d4e7b 删除role时提醒那些 主机上的系统用户会被删除, (需求:http://bbs.jumpserver.org/read/325.html) 2016-01-05 17:22:25 +08:00
ibuler
e9212d5ce6 fix普通用户大小修改 2016-01-04 17:15:33 +08:00
ibuler
bc2345ba14 Merge branch 'master' of github.com:ibuler/jumpserver 2016-01-04 16:38:20 +08:00
wangyong
c2818870d3 fix aliyun disk bug 2016-01-04 16:37:02 +08:00
ibuler
38b0f2f5f9 Merge pull request #34 from t57root/forget_password_vul
当有用户uuid 或账号姓名电邮地址信息时,可以修改任意账号的密码
2016-01-04 15:05:31 +08:00
ParInshOvGotQuep
79300b752b 修复找回密码及静态key问题 2016-01-04 13:56:47 +08:00
yumaojun
ed6559bbe9 修改前端禁止root的正则表达式,做精确匹配 2016-01-04 13:17:58 +08:00
yumaojun
0821e7cf41 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2016-01-04 13:13:35 +08:00
yumaojun
38f72a85e4 修改前端禁止root的正则表达式,做精确匹配 2016-01-04 13:13:13 +08:00
ibuler
ccc2bcf066 Merge branch 'dev' 2016-01-04 12:08:14 +08:00
ibuler
d9d009ab1f Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2016-01-04 12:08:02 +08:00
ibuler
f268af945f 修复web执行命令没有结果 2016-01-04 12:01:47 +08:00
yumaojun
34e8b32180 添加系统用户删除 提醒 2016-01-04 11:47:50 +08:00
yumaojun
c6626e83f2 禁止添加root用户作为系统用户 2016-01-04 11:43:17 +08:00
ibuler
4e5c501041 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2016-01-03 22:03:26 +08:00
yumaojun
0832ea97b1 去除sudo 添加和修改是 空格会被识别为 分隔符的问题 2016-01-03 21:11:55 +08:00
ibuler
7bef517518 支持修改窗口大小 2016-01-01 22:38:50 +08:00
ibuler
ea973bbb52 修复交换机小bug 2015-12-31 22:37:01 +08:00
ibuler
b673fec532 添加失败点击提示 2015-12-31 19:12:17 +08:00
ibuler
7e185197aa Merge branch 'sh' 2015-12-31 16:42:21 +08:00
ibuler
5e86b06db8 添加调试 2015-12-31 16:41:52 +08:00
ibuler
2e2b065c42 Merge pull request #31 from t57root/modify-default-shell
修改普通用户默认shell为connect.py,修复jailbreak
2015-12-31 16:41:22 +08:00
ibuler
91e006cfb0 Merge branch 'sh1' into sh 2015-12-31 16:35:33 +08:00
ibuler
b1a36bbb11 fix 2015-12-31 16:35:17 +08:00
ParInshOvGotQuep
ac40098ac5 修改硬编码的shell路径 2015-12-31 16:24:46 +08:00
ibuler
6ff030847e Merge branch 'sh1' into sh 2015-12-31 16:18:50 +08:00
ibuler
a12e401f15 fix comment 2015-12-31 12:54:15 +08:00
ibuler
49f2f92a9c fix输入换行 2015-12-31 11:20:41 +08:00
ibuler
d1ac7ca647 Merge branch 'master' into dev 2015-12-31 10:20:08 +08:00
wangyong
f6c26a201e 修改硬盘不显示问题 2015-12-30 22:35:32 +08:00
ParInshOvGotQuep
90b875adae 修改普通用户默认shell为connect.py,修复jailbreak 2015-12-30 19:15:33 +08:00
ibuler
7be7772af6 修复授权修改显示ip 2015-12-30 15:38:28 +08:00
ibuler
2fa1d7a95b fix connect.py 字符报错 2015-12-30 12:36:34 +08:00
ibuler
6e747fa299 Merge branch 'master' of github.com:ibuler/jumpserver 2015-12-30 12:12:00 +08:00
ibuler
86cc963673 更换alert我layer 2015-12-30 12:11:39 +08:00
ibuler
e776c3c5f9 Update README.md 2015-12-29 22:54:55 +08:00
wangyong
33da9fd143 fix hostname unicode 2015-12-29 14:20:47 +08:00
ibuler
09416286bf fix install port bug 2015-12-29 13:09:05 +08:00
ibuler
c5f9db450d 添加查看quickstart提示 2015-12-29 11:37:52 +08:00
ibuler
1e93b13b4c Merge branch 'master' of github.com:ibuler/jumpserver 2015-12-29 11:13:04 +08:00
ibuler
48cf64cd3a port int 2015-12-29 11:12:41 +08:00
ibuler
ff3666c6af Update README.md 2015-12-28 11:08:23 +08:00
ibuler
3918025ffd Update README.md 2015-12-28 10:53:00 +08:00
ibuler
1b7ee3b575 返回上个版本 2015-12-26 11:09:48 +08:00
ibuler
b370f01551 Merge branch 'master' into dev 2015-12-26 10:53:01 +08:00
ibuler
18da6f69a2 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-26 10:52:57 +08:00
yumaojun
d040e2719f 修复Role 删除时 秘钥问题等异常引起的Bug 2015-12-26 10:12:00 +08:00
ibuler
9eb64466bc fix user perm group perm 2015-12-25 20:26:31 +08:00
ibuler
9fc0c9da06 Merge branch 'master' of github.com:ibuler/jumpserver 2015-12-24 11:31:31 +08:00
ibuler
f63b75c01b fix 随机密码默认长度 2015-12-24 11:31:05 +08:00
ibuler
aa1dea1b08 Merge pull request #26 from wcc526/master
fix remote ip bug
2015-12-23 15:24:19 +08:00
chi-chi weng
71116bc525 fix remote ip bug
fix remote ip bug
2015-12-23 15:17:14 +08:00
ibuler
8f1e8af2e1 Merge branch 'dev' 2015-12-22 23:12:08 +08:00
ibuler
c691759af8 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-22 23:12:02 +08:00
yumaojun
d604639b96 修复添加role 时 因私钥格式不对而引起的bug(2) 2015-12-22 23:05:38 +08:00
ibuler
cde3185a20 Merge branch 'dev' 2015-12-22 22:50:17 +08:00
ibuler
40ae57d7ea Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-22 22:50:08 +08:00
ibuler
9efd42d4ed 添加team照片 2015-12-22 22:49:59 +08:00
yumaojun
3248ee1a3d 修复添加role 时 因私钥格式不对而引起的bug 2015-12-22 22:39:59 +08:00
ibuler
13d325e259 捕捉smtp异常 2015-12-22 17:18:53 +08:00
ibuler
79994e13c7 添加交流群 2015-12-22 15:57:16 +08:00
ibuler
3f9a9157f4 添加交流群 2015-12-22 15:56:39 +08:00
ibuler
7adcb11f1d comment newline 2015-12-21 16:58:30 +08:00
ibuler
42f6392cc4 fix install script bug 2015-12-21 16:10:07 +08:00
ibuler
835e32b1cc service.sh 脚本修改 2015-12-21 14:28:12 +08:00
ibuler
a7e2592fa8 fix install bug 2015-12-21 12:18:36 +08:00
ibuler
f17001e86c fix comment 2015-12-21 11:12:52 +08:00
ibuler
7113852cd6 update README 2015-12-21 10:25:51 +08:00
ibuler
7d85c0393f fix readme 2015-12-20 21:20:08 +08:00
ibuler
48af55adab 添加readline 2015-12-20 19:30:42 +08:00
ibuler
c36267dc17 rebase version 2015-12-20 19:09:18 +08:00
ibuler
b7eb95f85f 修复安装bug 2015-12-20 17:45:57 +08:00
ibuler
08cbaa1622 fix sudo bug 2015-12-20 00:30:31 +08:00
ibuler
a73fa7811c Merge branch 'dev' 2015-12-19 22:52:16 +08:00
wangyong
0d5fa418af Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-19 22:49:46 +08:00
wangyong
cf59ebf1aa fix asset add batch bug 2015-12-19 22:49:39 +08:00
ibuler
52566cb562 modify service.sh 2015-12-19 22:08:56 +08:00
ibuler
75b307105b fix 2015-12-19 22:00:54 +08:00
ibuler
998281d86d Merge branch 'dev' of git.coding.net:jumpserver/jumpserver 2015-12-19 21:57:15 +08:00
ibuler
8494989715 修改install 2015-12-19 21:56:42 +08:00
jumpserver
b73527fb33 update jumpserver.conf 2015-12-19 21:39:56 +08:00
ibuler
f18c68b8ba Update jumpserver.conf 2015-12-19 21:38:23 +08:00
ibuler
c8c0366b39 Merge with dev 2015-12-19 21:14:16 +08:00
ibuler
22c90eecfc 修改安装脚本 2015-12-19 21:12:47 +08:00
ibuler
3695989866 fix install bug 2015-12-19 20:31:06 +08:00
ibuler
80c5acef81 Merge branch 'dev' 2015-12-19 17:57:51 +08:00
ibuler
2cb6a1aac0 merge with github master 2015-12-19 17:46:30 +08:00
ibuler
39b7a7290f merge with dev 2015-12-19 17:30:21 +08:00
ibuler
7b792907d3 merge with dev 2015-12-19 17:27:12 +08:00
ibuler
c103738302 merge with dev 2015-12-19 17:26:34 +08:00
ibuler
89901fa7fb fix url error 2015-12-19 17:13:37 +08:00
ibuler
3176608eaa Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-19 17:09:55 +08:00
ibuler
94d0f7ebea 添加作者 2015-12-19 17:09:43 +08:00
wangyong
04eb4484da Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-19 17:01:23 +08:00
wangyong
9ffc887adf fix common user asset list search bug 2015-12-19 17:01:13 +08:00
ibuler
f5b97c57f8 fix readme pic width 2015-12-19 16:11:43 +08:00
ibuler
46c17b055e fix readme pic width 2015-12-19 16:10:08 +08:00
ibuler
b5bbbf7eb3 fix 2015-12-19 15:26:25 +08:00
ibuler
87f5a531bb fix 2015-12-19 15:24:50 +08:00
ibuler
3afeda8b19 fix 2015-12-19 15:20:49 +08:00
ibuler
832dc0b0e1 修改readme 2015-12-18 23:45:12 +08:00
ibuler
13cecd9b91 修改readme 2015-12-18 23:42:17 +08:00
ibuler
cba7195670 chmod +x script 2015-12-18 23:03:48 +08:00
ibuler
446a51122f 添加doc 2015-12-18 18:55:42 +08:00
ibuler
e3c0757166 添加doc 2015-12-18 18:52:32 +08:00
ibuler
0715e11c05 fix install bug 2015-12-18 18:35:07 +08:00
ibuler
1fc86733ba fix install bug 2015-12-18 18:27:12 +08:00
ibuler
4c002afde1 fix install bug 2015-12-18 18:14:12 +08:00
ibuler
caa0e29564 fix install bug 2015-12-18 18:02:46 +08:00
ibuler
0896c9ab3f fix install bug 2015-12-18 17:59:05 +08:00
ibuler
c56c44f8c0 fix install bug 2015-12-18 17:56:00 +08:00
ibuler
655bfc855e fix install bug 2015-12-18 17:49:59 +08:00
ibuler
439efa1c83 fix install bug 2015-12-18 17:42:25 +08:00
ibuler
59591c2a13 fix install bug 2015-12-18 17:39:22 +08:00
ibuler
e452c6d70a fix install bug 2015-12-18 16:52:50 +08:00
ibuler
12967bb6f9 fix install bug 2015-12-18 16:52:36 +08:00
广宏伟
6798dd8686 添加安装脚本 2015-12-18 16:26:17 +08:00
ibuler
d72a8ac5a9 修改 readme 2015-12-17 18:56:10 +08:00
ibuler
f38f809337 修改目录结构 2015-12-17 17:37:26 +08:00
ibuler
c78ab8edf7 fix 日志记录名称 2015-12-17 17:35:57 +08:00
ibuler
e24003df97 Update connect.py
修复 remote ip抓取问题
2015-12-17 17:12:26 +08:00
ibuler
f8213a1a2a user update password null 2015-12-17 11:05:43 +08:00
ibuler
da7cc102a3 fix null bug 2015-12-17 10:52:48 +08:00
ibuler
ea29305f8b Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-17 10:08:49 +08:00
ibuler
9fb71ade71 remove install wizzrd 2015-12-17 10:08:38 +08:00
ibuler
5740dec7b4 add install wizzrd 2015-12-17 10:04:19 +08:00
wangyong
7c742bf061 fix del idc failed 2015-12-16 22:24:29 +08:00
yumaojun
a30ec0c75e 修复 sudo 规则删除bug 2015-12-16 18:34:19 +08:00
yumaojun
b8abedc5b9 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-15 22:16:15 +08:00
ibuler
ef7f42cf6d reset password fix 2015-12-15 19:36:33 +08:00
ibuler
e817715da3 fix asset update bug 2015-12-15 17:35:02 +08:00
ibuler
9c0825ce70 fix more role open exec window width bug 2015-12-15 16:46:14 +08:00
ibuler
2e81f0cf95 fix nav active bug 2015-12-15 14:48:37 +08:00
ibuler
c4b099d2a5 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-15 14:45:41 +08:00
ibuler
f3606dfd96 add install wizzird 2015-12-15 14:45:37 +08:00
yumaojun
05f80f743b Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-14 20:28:39 +08:00
wangyong
f249509817 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-14 18:42:27 +08:00
wangyong
c6e0b2007d alert idc|group length 2015-12-14 18:42:17 +08:00
ibuler
2496f1c654 asset udpate url bug 2015-12-14 18:28:53 +08:00
ibuler
1a32fb5da1 fix del role url bug 2015-12-14 17:13:07 +08:00
ibuler
be2f0b2172 fix log kill and asset detail bug 2015-12-14 16:54:49 +08:00
ibuler
5e5aab7962 fix asset upload bug 2015-12-14 16:26:12 +08:00
ibuler
a8bdc37780 fix group list bug 2015-12-14 15:57:46 +08:00
广宏伟
3260584f90 修改jumpserver.conf注释 2015-12-14 14:52:35 +08:00
ibuler
b58ff14ed1 role fix to sys user 2015-12-14 14:36:42 +08:00
ibuler
db13b7a3e9 主机组列表编辑连接修改 2015-12-12 23:47:28 +08:00
ibuler
3150461d12 bug fix 2015-12-12 23:37:28 +08:00
ibuler
97bfbe24b8 bug fix 2015-12-12 23:09:58 +08:00
ibuler
30b5f74c51 bug fix 2015-12-12 23:01:59 +08:00
ibuler
69aba4643e bug fix 2015-12-12 22:57:42 +08:00
ibuler
f849ffef3e url modify 2015-12-12 19:30:39 +08:00
ibuler
6d5513c69e open in current window 2015-12-11 17:40:52 +08:00
ibuler
b27e2075ff bug fix 2015-12-11 17:28:01 +08:00
ibuler
9979fdf6de bugfix 2015-12-11 15:32:06 +08:00
yumaojun
b655b064df Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-11 14:38:24 +08:00
yumaojun
974619d1ce fixed rule and role detail page not id bug 2015-12-11 14:38:10 +08:00
ibuler
280560f286 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-11 14:03:12 +08:00
wangyong
7ce12af146 fix none bug 2015-12-11 14:02:41 +08:00
ibuler
b7bf6b4cc0 Merge pull request #23 from hailwind/master
add choose group to execute commands. 增加组执行命令功能
2015-12-11 12:05:15 +08:00
ibuler
ba955df493 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-11 11:45:25 +08:00
ibuler
34cbb13b8a bug fix 2015-12-11 11:45:19 +08:00
yumaojun
aa262d0430 fixed perm page 2015-12-10 22:58:04 +08:00
yumaojun
4294ecaf26 修复了rule 和 role 的detail 页面 2015-12-10 22:56:24 +08:00
ibuler
014d0935ba bug fix 2015-12-10 21:55:21 +08:00
ibuler
e65cad5074 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-10 17:51:23 +08:00
yumaojun
b0f2b346f9 修复了rule 和 role 的detail 页面 2015-12-10 17:44:45 +08:00
ibuler
affd9aadb5 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-10 14:42:03 +08:00
ibuler
23051a4a05 bug fix 2015-12-10 14:42:01 +08:00
yumaojun
e9fe871af3 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-10 14:40:45 +08:00
yumaojun
80b1bda51c update... 2015-12-10 14:40:29 +08:00
ibuler
e117cd003f Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-10 14:10:56 +08:00
ibuler
e03b4722b0 defend attack 2015-12-10 14:10:47 +08:00
yumaojun
b5a3fb44c3 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-10 01:15:46 +08:00
yumaojun
4903a17104 Merge branch 'map_perm' into dev
# Conflicts:
#	jperm/ansible_api.py
#	jperm/views.py
2015-12-10 01:15:25 +08:00
yumaojun
a068498561 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev
# Conflicts:
#	jperm/ansible_api.py
2015-12-10 00:17:01 +08:00
halcyon
1a3541e575 fix bugs 2015-12-10 00:13:50 +08:00
yumaojun
49267b57e4 1. 角色添加和角色修改, Server 端 输入验证
2. 日志打印
2015-12-10 00:10:39 +08:00
ibuler
d337b929ef perm edit fix 2015-12-09 17:27:13 +08:00
ibuler
f74b15c1bf perm edit fix 2015-12-09 16:42:48 +08:00
ibuler
d3fd9e05ca ansible api fix 2015-12-09 14:50:48 +08:00
ibuler
16b94c1089 bug fix 2015-12-09 11:49:30 +08:00
ibuler
d6b4c7c485 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-08 23:19:42 +08:00
ibuler
0309213ccc bug fix 2015-12-08 23:19:39 +08:00
ibuler
e408414631 bug fix 2015-12-08 19:25:11 +08:00
kelianchun_miller
80ffb9d762 update connect.py 2015-12-08 11:44:42 +08:00
ibuler
bd2a3e6119 bugfix 2015-12-08 11:32:27 +08:00
ibuler
95e8115045 bug fix 2015-12-07 23:29:19 +08:00
ibuler
b7cfac4d1f bug fix 2015-12-07 23:21:10 +08:00
ibuler
5be2633795 bug fix 2015-12-07 18:06:07 +08:00
ibuler
cb12b83e47 bug fix 2015-12-07 17:41:29 +08:00
ibuler
cd798daf0a bug fix 2015-12-07 17:30:57 +08:00
wangyong
7f6f46b662 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-06 23:48:46 +08:00
wangyong
e78d7b0219 cu asset page and sth 2015-12-06 23:48:39 +08:00
yumaojun
58082179fe 1. 用户的批量回收, 角色删除会回收推送的角色 2015-12-06 23:44:13 +08:00
ibuler
622611498c fix bug 2015-12-06 23:33:23 +08:00
ibuler
b346e1c480 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-06 22:00:55 +08:00
ibuler
bf23d6d7aa fix some bug 2015-12-06 22:00:44 +08:00
kelianchun_miller
3d0b8917fa update connect.py 2015-12-06 20:56:28 +08:00
yumaojun
8723d673d7 1. 计算该角色有哪些主机没推送时,使用交集计算(原来是差集)
2. 修改rule   detail页面 不计算,经返回rule 记录的信息
3. 修改role   detail页面 不计算,经返回rule 记录的信息
4. 添加了 推送主机上的用户回收功能
5. TODO:  页面的美观展示,与 实现 用户的批量回收。
2015-12-06 18:07:57 +08:00
ibuler
35c818f4a0 用户详情 2015-12-06 00:28:43 +08:00
ibuler
532646a431 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-05 17:55:49 +08:00
ibuler
0fd7b980de fix some bug 2015-12-05 17:55:45 +08:00
wangyong
2f0e91a538 fix bug 2015-12-05 16:57:47 +08:00
wangyong
0f6f3bdb9a Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-05 16:15:33 +08:00
wangyong
2c3e681942 fix bugs 2015-12-05 16:15:19 +08:00
ibuler
34ccaeb10f fix push role bug 2015-12-05 12:03:18 +08:00
wangyong
8f985ade97 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-12-05 11:09:41 +08:00
wangyong
9b7ef1139e fix bugs 2015-12-05 11:09:34 +08:00
ibuler
2829a7ad31 fix asset admin count 2015-12-04 18:49:17 +08:00
ibuler
b5325fd0ac bug fix for rule add check 2015-12-04 18:24:20 +08:00
ibuler
69bbdab450 完成上传下载 2015-12-04 13:42:05 +08:00
ibuler
6975acfc8a 文件上传日志 2015-12-03 23:24:34 +08:00
ibuler
9233fef63f exec log 2015-12-03 19:10:37 +08:00
ibuler
33663783cc 恢复错误修复 2015-12-03 18:49:04 +08:00
ibuler
6f4fd18c47 add 2015-12-03 18:38:06 +08:00
ibuler
7decfdc37f col-lg -> col-sm to 2015-12-03 18:37:18 +08:00
ibuler
267bb02417 修改exec 和 MyRUnner 2015-12-03 16:53:39 +08:00
ibuler
255e3a043a 修改支持选中执行 2015-12-03 10:38:10 +08:00
ibuler
6080a861db Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-02 23:50:31 +08:00
ibuler
4959073a33 web 批量执行命令 2015-12-02 23:50:20 +08:00
halcyon
f6dbec1436 day update 2015-12-02 23:39:17 +08:00
ibuler
88fbcabcbb exec-cmd tempalate 2015-12-02 19:17:12 +08:00
ibuler
1f26e49fb8 完成web批量命令执行 2015-12-02 19:16:05 +08:00
ibuler
104ee779e7 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-12-02 15:42:24 +08:00
ibuler
77da01d1ee 完成上传下载 2015-12-02 15:41:39 +08:00
ibuler
0e6fd89f0b 批量上传下载 2015-12-02 13:35:06 +08:00
halcyon
dd912e8958 asset list and add system_arch 2015-12-01 23:39:49 +08:00
ibuler
e95c47ff6a upload 2015-12-01 19:23:35 +08:00
ibuler
6b7937c639 普通用户页面 2015-12-01 13:21:42 +08:00
ibuler
f72c5753c9 添加推送 2015-12-01 11:58:11 +08:00
yumaojun
bf98aa5464 sudo push 2015-12-01 11:20:43 +08:00
yumaojun
f3102e3b5b erge laoguang. 2015-11-30 23:10:31 +08:00
yumaojun
e0aaba2cf5 no change... 2015-11-30 23:08:30 +08:00
ibuler
4b36fc540d role推送基本完成 2015-11-30 23:04:02 +08:00
yumaojun
7cafbde5b1 update sudo 2015-11-30 22:55:40 +08:00
ibuler
7106e915ef modify sudo 2015-11-30 19:51:34 +08:00
ibuler
f7c8ad6f38 修改推送用户 2015-11-30 19:06:25 +08:00
Alex Wang
3f451158f0 add choose group to execute commands. 2015-11-30 15:15:05 +08:00
ibuler
8dd4a9fce1 push role 更改 2015-11-29 19:02:13 +08:00
ibuler
52c4395b68 去掉runas 2015-11-29 16:56:39 +08:00
ibuler
49fbae4fad Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-29 16:38:48 +08:00
ibuler
2d91f1ab38 校验推送 2015-11-29 16:38:40 +08:00
yumaojun
4d844548c2 fixed merge... 2015-11-29 15:50:36 +08:00
ibuler
29e1090d2c sudo添加runas 2015-11-29 15:18:05 +08:00
ibuler
98f0655da4 fix rule and role bug 2015-11-29 11:33:19 +08:00
yumaojun
b241d6d148 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-11-28 23:00:40 +08:00
yumaojun
b7d9e41b43 1. 修复角色推送 step3 失败 2015-11-28 22:54:59 +08:00
ibuler
3f000b9d54 修改role添加显示 2015-11-28 22:53:27 +08:00
ibuler
4672884255 修改role添加显示 2015-11-28 22:45:40 +08:00
ibuler
a0ae7ff139 修改getresource 2015-11-28 22:08:47 +08:00
ibuler
8ac369b925 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-28 21:23:01 +08:00
ibuler
117f01e71f 中文播放bug fix 2015-11-28 21:13:44 +08:00
yumaojun
38416fbebb merge perm 2015-11-28 21:02:23 +08:00
yumaojun
39a0350e08 1. 完成Sudo 规则的 角色授权
2. 角色详情里面 新增 推送详情
3. 角色推送 支持计算与叠加
2015-11-28 19:33:21 +08:00
ibuler
a099d2a25a 中文播放bug 2015-11-27 18:51:15 +08:00
ibuler
bd885da179 recoard 特殊字符去掉 2015-11-27 17:53:11 +08:00
ibuler
35db337f79 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-27 16:16:47 +08:00
ibuler
c3f1e0e06e 批量执行命令记录日志 bug fix 2015-11-27 16:16:38 +08:00
ibuler
b64ab276fb 修改批量执行命令2 2015-11-27 13:24:57 +08:00
ibuler
66610fb3e7 修改批量执行命令 2015-11-27 12:20:08 +08:00
halcyon
09dcdfa318 fix some bugs and hehe 2015-11-26 23:42:58 +08:00
ibuler
c574bbcb96 fix date change bug 2015-11-26 23:26:11 +08:00
ibuler
61deed70ad merge with connect 2015-11-26 21:47:37 +08:00
ibuler
601747ac49 fix 2015-11-26 21:01:39 +08:00
ibuler
2afce31e95 删掉添加导航 2015-11-26 20:19:54 +08:00
ibuler
ba08602e20 添加登陆方式 2015-11-26 19:49:23 +08:00
ibuler
19dcf171e6 弹窗title修改 2015-11-26 19:06:17 +08:00
kelianchun_miller
1bbd685a23 update connect.py 2015-11-26 18:19:36 +08:00
ibuler
02c3bc75f3 connect.py 回滚 2015-11-26 17:42:03 +08:00
yumaojun
951467f8ca 1. 新增 PermPush表, 用于记录角色的推送记录
2. 角色权限 以来 sudo 完成。
2015-11-26 17:23:16 +08:00
ibuler
cdfea145d9 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-26 17:18:47 +08:00
kelianchun_miller
431ad2940b update connect.py 2015-11-26 17:17:37 +08:00
ibuler
72b8ac2a50 Merge branch 'bug' into dev 2015-11-26 17:10:25 +08:00
ibuler
d3153e9bb4 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-26 17:10:18 +08:00
ibuler
5045d66c1c fix 2015-11-26 17:10:09 +08:00
kelianchun_miller
e71bbd4122 update connect.py 2015-11-26 14:26:58 +08:00
kelianchun_miller
c4160e1d50 Accept Pull Request #2 : (kelianchun_miller:dev -> jumpserver:dev)
Pull Request: 提交transport和命令解析
Created By: @kelianchun_miller
Accepted By: @kelianchun_miller
URL: https://coding.net/u/jumpserver/p/jumpserver/git/pull/2
2015-11-26 14:25:07 +08:00
ibuler
46245ab4aa fix record bug 2015-11-26 13:21:05 +08:00
kelianchun_miller
8880b5ff4f update connect.py
增加transport连接方式,同时增加了替换符号的命令处理
2015-11-25 19:02:51 +08:00
ibuler
2c0d42e0da fix some bug 2015-11-25 18:59:12 +08:00
kelianchun_miller
1d70a23859 update connect.py 2015-11-25 18:52:18 +08:00
ibuler
0335bc26ca fix rule js bug 2015-11-25 16:37:17 +08:00
ibuler
60166ac008 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-25 16:06:30 +08:00
ibuler
f760df1e34 角色key问题修复 2015-11-25 16:01:07 +08:00
ibuler
6fe6342ca4 角色key问题修复 2015-11-25 14:59:57 +08:00
liuzheng712
175f2702d1 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-25 14:52:30 +08:00
ibuler
a7db713b1e Merge branch 'exec_cmd' into dev 2015-11-25 10:01:06 +08:00
yumaojun
1a0c9cd4e6 1. 新增 PermSudo表, 用于记录 sudo别名
2. 实现Sudo表对应的 添加,显示,更新,删除页面
3. 添加角色时,需要选择对应的 sudo别名
2015-11-24 22:03:58 +08:00
wangyong
742e56fc5f Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-11-24 19:04:28 +08:00
wangyong
0262d94dd3 fix disk detail bug 2015-11-24 19:04:16 +08:00
ibuler
32d036c2b0 修改setting字段 2015-11-24 17:38:27 +08:00
ibuler
1a63d32fe3 fix bug 2015-11-24 16:31:06 +08:00
ibuler
ec9b00e255 fix bug 2015-11-24 13:37:36 +08:00
ibuler
d7cb549eac Merge branch 'exec_cmd' into dev 2015-11-24 13:31:55 +08:00
ibuler
168157bfe1 fix 2015-11-24 13:31:48 +08:00
ibuler
7cbb7718eb fix bug 2015-11-24 12:03:38 +08:00
ibuler
2d65265fc6 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-24 12:01:02 +08:00
ibuler
30fe9f5236 fix websocket授权 2015-11-24 11:58:42 +08:00
wangyong
49821062fb Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-11-24 11:22:01 +08:00
wangyong
99439be04f add update all button 2015-11-24 11:20:01 +08:00
ibuler
17ccac92ee fix 加密bug 2015-11-24 11:01:54 +08:00
wangyong
dd4c243d7c Merge branch 'cmdb' of https://git.coding.net/jumpserver/jumpserver into cmdb 2015-11-24 09:53:50 +08:00
halcyon
34031bfd6b fix bugs 2015-11-23 23:55:19 +08:00
ibuler
a7a030fedd fix bug 2015-11-23 23:07:58 +08:00
ibuler
4c50551249 fix some bug 2015-11-23 19:15:52 +08:00
ibuler
483ca9677c 资产列表添加连接 2015-11-23 18:39:38 +08:00
ibuler
f00a2e003d Merge branch 'dev' into exec_cmd 2015-11-23 15:36:24 +08:00
ibuler
0b878216ed Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-23 15:34:34 +08:00
ibuler
f49a92e742 webscoket授权 2015-11-23 15:34:28 +08:00
kelianchun_miller
ee218a5fed update connect.py 2015-11-23 12:47:02 +08:00
wangyong
a9196ef149 merge cmdb | add asset update batch and crontab 2015-11-23 11:12:14 +08:00
wangyong
04bf37b52a Merge branch 'cmdb' into dev 2015-11-23 11:06:41 +08:00
wangyong
5a3c11f619 asset update batch and crontab 2015-11-22 23:53:25 +08:00
liuzheng712
c89d43d269 update, now Mac to develop will be all egg pain 2015-11-22 22:20:55 +08:00
ibuler
12f33176bf Merge branch 'dev' into exec_cmd 2015-11-22 21:59:18 +08:00
ibuler
eb779e5bf6 修改run_websocket 2015-11-22 21:59:07 +08:00
ibuler
47ace5d654 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-22 20:47:45 +08:00
ibuler
70db8c69d2 global SSH_FLAG to self.ssh_flag 2015-11-22 20:45:48 +08:00
liuzheng712
962d16172c Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-22 20:35:03 +08:00
kelianchun_miller
5f9e675d00 update connect.py 2015-11-22 20:29:35 +08:00
ibuler
4cdc7f33ce 添加注释 2015-11-22 20:20:32 +08:00
kelianchun_miller
c359d1e264 update connect.py 2015-11-22 20:20:04 +08:00
kelianchun_miller
0c42d039ff update connect.py 2015-11-22 20:18:33 +08:00
kelianchun_miller
55503c89e9 update connect.py 2015-11-22 19:15:58 +08:00
wangyong
b8cb6f4246 Merge branch 'dev' into cmdb 2015-11-22 18:58:31 +08:00
wangyong
d25e23623a fix bugs 2015-11-22 18:57:47 +08:00
ibuler
a143797ac9 修改gen_resource api 2015-11-22 17:56:38 +08:00
liuzheng712
09de12c02a index show assets, need discussion 2015-11-22 13:04:11 +08:00
liuzheng712
246e8770ac http://localhost:8000/juser/user_detail/?id=5001 fix 2015-11-21 22:40:28 +08:00
liuzheng712
ea059790a9 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-21 21:24:17 +08:00
liuzheng712
6ae4907520 update 2015-11-21 21:24:01 +08:00
ibuler
5f768444ac Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-21 21:07:39 +08:00
ibuler
93481a98db 完成登陆,批量执行命令流 2015-11-21 19:20:11 +08:00
wangyong
0ebb7c4f9a merge cmdb 2015-11-21 18:37:40 +08:00
wangyong
652ea98a2b alert ip -> hostname 2015-11-21 18:28:25 +08:00
ibuler
877383679c 添加api注释 2015-11-21 14:45:20 +08:00
ibuler
e041a49ad7 添加资产权限查询,添加生成resource文件api 2015-11-21 14:42:53 +08:00
ibuler
dc06426ada 完成授权查询api 2015-11-21 12:41:17 +08:00
ibuler
1478ac18fa 用户和组查询api完成 2015-11-21 11:53:36 +08:00
ibuler
558309599c 添加授权查询api 2015-11-21 00:42:54 +08:00
ibuler
40d1eb37dc Merge branch 'exec_cmd' into dev 2015-11-20 23:12:48 +08:00
ibuler
b8d6c5d007 删除遗留的model SysUser PermLog 2015-11-20 23:12:33 +08:00
ibuler
85a9da0e8a bug fix 2015-11-20 21:59:09 +08:00
ibuler
faa0275700 Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-20 21:48:58 +08:00
ibuler
eea56ea0e5 定义通用模块执行 2015-11-20 21:30:57 +08:00
ibuler
ab313aacd8 定义Command前 2015-11-20 18:42:44 +08:00
yumaojun
a88ee0725d 1. 在授权规则添加页面 通过js 给予用户输入提醒 2015-11-20 14:07:17 +08:00
yumaojun
e57c6a9d2e 1. 在授权规则添加页面 通过js 给予用户输入提醒 2015-11-20 14:03:05 +08:00
liuzheng712
f863f4b7ae 登陆web的密码(如不修改请留空) 2015-11-20 11:26:07 +08:00
liuzheng712
4bbae38150 confirm alert 2015-11-20 11:16:47 +08:00
liuzheng712
31807b674b Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-20 11:12:10 +08:00
liuzheng712
75fbd9e615 annotation the Asset 2015-11-20 11:08:39 +08:00
liuzheng712
42ec624a91 group add and remove fixed 2015-11-20 11:04:22 +08:00
halcyon
b4c13069c0 batch asset alert 2015-11-20 00:03:36 +08:00
ibuler
6b1b33481c 一些js, setting的KEY_DIR, 下载秘钥鉴定 2015-11-19 23:11:00 +08:00
halcyon
1471e0a247 merge cmdb and hehe 2015-11-19 22:06:12 +08:00
halcyon
a04bb100bf Merge branch 'cmdb' into dev 2015-11-19 22:01:38 +08:00
halcyon
f9b1d3059a fix bugs 2015-11-19 21:58:44 +08:00
ibuler
bddb689e86 merge with exec_cmd 2015-11-19 21:57:17 +08:00
yumaojun
757f7beeaf update merge conflict. 2015-11-19 13:20:27 +08:00
yumaojun
6582ee1624 1. 更新 和 添加 (role 和 rule )操作 以 msg 的方式 刷新页面 2015-11-18 22:32:21 +08:00
yumaojun
04e7073aca 1. fixed ansible api auto load local host file (/etc/ansible/hosts) bug with null list 2015-11-18 19:03:13 +08:00
yumaojun
2ccd072416 1. delete role filed : is_screty_key
2. rule edit page  add selected mark
3. role add page  consider user use_default_auth attribute
4. role password use jumpserver api CRYPTO to crypt
5. fixed ansible api auto load local host file (/etc/ansible/hosts) bug
6. ansible api command and task  interface add pattern  default argument( pattern='*')
2015-11-18 17:16:29 +08:00
ibuler
79ecbc83ae bug fix 2015-11-18 15:23:56 +08:00
ibuler
2a6051ae35 merge with dev 2015-11-18 15:22:26 +08:00
ibuler
cf8e366ae1 修改connect方法 2015-11-18 15:15:08 +08:00
halcyon
d42197db3a fix tower bugs 2015-11-17 23:34:13 +08:00
ibuler
8017b1b479 解决jperm key dir 和 ansible api问题 2015-11-17 23:32:24 +08:00
yumaojun
375a243131 Merge remote-tracking branch 'origin/map_perm' into dev 2015-11-17 22:18:17 +08:00
yumaojun
70e6904eb7 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into dev 2015-11-17 22:17:18 +08:00
ibuler
42745c52df Merge branch 'dev' of git.coding.net:jumpserver/jumpserver into dev 2015-11-17 21:41:31 +08:00
ibuler
8479989ea8 更换nav ico, 修复需要登陆,登陆后跳转到登陆前页面 2015-11-17 21:41:03 +08:00
liuzheng712
ee7675f7ae fix when ./keys/user/ didn't exist bug 2015-11-17 21:06:38 +08:00
liuzheng712
37b91c40b6 url 2015-11-17 20:27:38 +08:00
liuzheng712
4027a87e7f user delete with alert confirm 2015-11-17 20:25:15 +08:00
liuzheng712
84992746de juser/change_info/ fix password can be ignore 2015-11-17 20:06:25 +08:00
liuzheng712
4397819b44 user.update bug fix 2015-11-17 19:55:38 +08:00
liuzheng712
ee443361b9 bug of user.update 2015-11-17 19:51:16 +08:00
ibuler
c00a55619c Update connect.py
去掉compress
2015-11-17 16:43:33 +08:00
yumaojun
e5c1071073 Merge branch 'dev' into map_perm 2015-11-17 14:10:46 +08:00
yumaojun
8d941dc028 update role edit and rule edit 2015-11-17 13:55:13 +08:00
yumaojun
11f6939b85 1. update role and update rule complete. 2015-11-17 13:48:19 +08:00
liuzheng712
1c3b66c1a8 user.update 2015-11-17 11:13:52 +08:00
liuzheng712
a99f024dfc user.update 2015-11-17 11:07:40 +08:00
ibuler
d58ba82388 添加ignore 2015-11-17 10:48:18 +08:00
liuzheng712
738c6353ef user.update 2015-11-17 10:44:34 +08:00
ibuler
dc547de592 修正文件目录 2015-11-17 10:27:41 +08:00
liuzheng712
adf20f188e user.update 2015-11-17 10:16:51 +08:00
liuzheng712
b9b965753b Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-17 09:53:48 +08:00
liuzheng712
036873e129 merge_dev 2015-11-17 09:53:43 +08:00
yumaojun
9d36dc9e39 1. push role bug , because the password push is mandatory, so just the push key is option. 2015-11-16 23:44:53 +08:00
ibuler
ce02d430ba deal_command添加到静态方法中 2015-11-16 23:33:44 +08:00
ibuler
4abf2b06d8 添加jumpserver.conf 2015-11-16 23:11:37 +08:00
ibuler
2d27e3fee0 恢复修改前 2015-11-16 22:46:36 +08:00
ibuler
67771105ae key目录变化 2015-11-16 22:45:41 +08:00
liuzheng712
de9ce6dd19 select all 2015-11-16 22:28:02 +08:00
ibuler
71ef4b079a Merge branch 'dev' of git.coding.net:jumpserver/Jumpserver into dev 2015-11-16 18:35:31 +08:00
ibuler
f0d901e136 修复分页bug 2015-11-16 18:34:51 +08:00
liuzheng712
d3dd9d94d3 update 2015-11-16 14:59:26 +08:00
liuzheng712
2938f7e506 update 2015-11-16 14:30:10 +08:00
liuzheng712
1f5fa9a9bd merge 2015-11-16 14:21:21 +08:00
liuzheng712
96ac65b346 udpate 2015-11-16 14:09:02 +08:00
yumaojun
c00e4c24a3 app jperm role and rule bug fixed... 2015-11-16 13:49:39 +08:00
yumaojun
844fe2c250 add asset ok ,pull 2015-11-15 23:43:19 +08:00
yumaojun
6da97c5403 merge dev ... 2015-11-15 23:30:37 +08:00
wangyong
1e41b79611 fix merge bug 2015-11-15 23:29:31 +08:00
ibuler
5d33493233 asset js 2015-11-15 23:21:59 +08:00
ibuler
38c65dbea8 暂时去掉普通用户的get.asset 2015-11-15 23:19:50 +08:00
yumaojun
218ba0d189 merge ... 2015-11-15 22:20:19 +08:00
yumaojun
a36294a529 new map 2015-11-15 22:12:28 +08:00
广宏伟
333ee713d4 解决分页bug,和random_pass 2015-11-15 18:20:34 +08:00
广宏伟
b8ae0ed565 some bug fix 2015-11-15 17:01:02 +08:00
ibuler
3b07198b8a merge with cmdb 2015-11-15 19:57:25 +08:00
yumaojun
8b94833ea0 1. role push function use password and public key completed 2015-11-15 12:38:57 +08:00
yumaojun
0862ffef98 merge map 2015-11-15 08:39:43 +08:00
yumaojun
95dc12a71b update models 2015-11-14 23:17:05 +08:00
wangyong
0f29745bfa asset add batch 2015-11-14 23:09:13 +08:00
yumaojun
012950cb68 merge update... 2015-11-14 23:07:08 +08:00
ibuler
f9c06c22ae 修改变量命名 2015-11-14 22:57:34 +08:00
yumaojun
83c2704d53 1. update role push 2015-11-14 22:54:29 +08:00
yumaojun
74b6b4dea2 1. rule push page completed 2015-11-14 21:52:09 +08:00
yumaojun
f6bc03324f 1. rule operations list add delete edit info page compeleted
2. rule operations list add delete edit info page compeleted
2015-11-14 21:13:02 +08:00
kelianchun_miller
3d4443e1b0 update connect.py
命令处理进一步处理
2015-11-14 11:45:56 +08:00
kelianchun_miller
366a5d5737 update connect.py 2015-11-14 11:24:31 +08:00
yumaojun
6d5d279f61 1. 增加授权规则的添加,删除,编辑,详情页面。
2. 修改nav.html中关于授权部分页面。
3. 修改Jasset APP 下的 view,使得数据库中存储的密码是明文。
2015-11-14 10:16:48 +08:00
ibuler
20970db404 修复环境变量大小写,修改terminal类 2015-11-13 21:15:41 +08:00
yumaojun
1eff4ab4ff add edit, detail page 2015-11-13 17:17:22 +08:00
yumaojun
1aed3b4d91 modify add rule html 2015-11-13 13:21:57 +08:00
yumaojun
9379bc1f6e merge failed, update.... 2015-11-13 13:06:17 +08:00
yumaojun
9ec2b9ff1d merge filed, update files... 2015-11-13 12:50:21 +08:00
yumaojun
4bfe326868 ansible api add get_host_info in Class Tasks 2015-11-13 00:34:32 +08:00
yumaojun
6074bb033d ansible api add get_host_info in Class Tasks 2015-11-13 00:31:27 +08:00
halcyon
ec7b543bf2 Merge branch 'cmdb' of https://git.coding.net/jumpserver/jumpserver into cmdb 2015-11-13 00:06:15 +08:00
halcyon
b7c3cc5532 add idc 2015-11-13 00:03:51 +08:00
Zi Chuanxiu
1c3d642be2 fixed ansible command variables bug. 2015-11-10 19:55:12 +08:00
Zi Chuanxiu
a2f84e943a update ansible api 2015-11-10 17:46:44 +08:00
ibuler
4ae36d319c 修复用户信息显示bug 2015-11-10 16:14:32 +08:00
ibuler
7fe0bb21b2 修改无日志记录提示 2015-11-10 16:00:05 +08:00
ibuler
0174da6c77 修改index页面 2015-11-10 15:50:35 +08:00
root
3d6d714495 asset alert history 2015-11-10 00:11:54 +08:00
ibuler
adc3703fba 添加绘图 2015-11-10 00:02:56 +08:00
wangyong
6de253df58 delete xlsx 2015-11-09 21:04:47 +08:00
ibuler
f31bea2c70 使用echart 2015-11-09 19:19:28 +08:00
liuzheng712
b61286891f update 2015-11-09 17:12:57 +08:00
liuzheng712
6db7642331 Merge branch 'dev' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ 2015-11-09 16:58:55 +08:00
ibuler
91b5d039cc 修改邮件settings 2015-11-09 16:28:10 +08:00
ibuler
5692e9dc86 添加资产的默认设置 2015-11-09 11:56:31 +08:00
ibuler
7849ff2b92 解决搜索分页问题 2015-11-09 10:53:15 +08:00
halcyon
e77ec102d9 just do it 2015-11-08 22:39:30 +08:00
ibuler
7ea23e0fcc 修复无法终结web进程的bug 2015-11-08 12:01:08 +08:00
ibuler
e60a3a697e 添加测试web terminal按钮 2015-11-07 17:41:16 +08:00
ibuler
d899360c34 增加websocket认证 2015-11-07 17:32:32 +08:00
ibuler
98c4d9bdba web terminal kill方法 2015-11-07 15:27:49 +08:00
ibuler
24730ebd0f web terminal记录日志 2015-11-07 13:38:50 +08:00
liuzheng712
cab346686d update 2015-11-07 10:27:55 +08:00
liuzheng712
d369d4b50f Merge branch 'jlog' of https://git.coding.net/jumpserver/jumpserver into NormalUserPageLZ
# Conflicts:
#	jumpserver/views.py
#	juser/models.py
#	juser/views.py
#	templates/jperm/perm_apply.html
2015-11-07 09:37:10 +08:00
liuzheng712
6897d063c2 error = '用户未激活' 2015-11-07 09:09:36 +08:00
liuzheng712
4ddddc134a 禁用 change to 启用 2015-11-06 23:13:02 +08:00
liuzheng712
44a330ac02 download page: waiting... 2015-11-06 23:04:09 +08:00
liuzheng712
f220ea367e upload file with user id path 2015-11-06 22:26:19 +08:00
ibuler
43c0770770 使用原生select 2015-11-06 12:09:23 +08:00
Zi Chuanxiu
82286ea7ed fix set host vars and add set group vars 2015-11-06 11:32:59 +08:00
ibuler
01d6f751b1 解决实时监控特殊字符,使用term.js 2015-11-06 10:42:10 +08:00
liuzheng712
7a25f348e4 upload file: save a copy in ./upload/%Y/%M/%D/. TODO: distinguish userID 2015-11-06 00:56:16 +08:00
liuzheng712
12a0a35adf upload page: need to fix click problem, want to show the list of machines, TODO 2015-11-06 00:21:59 +08:00
Zi Chuanxiu
0f8abb6026 update 2015-11-05 22:53:19 +08:00
Zi Chuanxiu
8e2f3fdecd update 2015-11-05 22:52:02 +08:00
Zi Chuanxiu
26e3634814 mapping model 2015-11-05 22:47:45 +08:00
ibuler
9a897e0cf4 修正导入select错误 2015-11-05 20:07:21 +08:00
ibuler
1bc15e57a6 完成webtermial demo 2015-11-05 18:31:00 +08:00
liuzheng712
09087a4d8f set user password , if you don't want to change the password , leave password empty 2015-11-05 14:30:59 +08:00
liuzheng712
6934380b9c jperm/perm_apply.html 2015-11-05 13:16:23 +08:00
ibuler
618aed3278 移动api中关于tty到connect.py 2015-11-04 17:04:25 +08:00
ibuler
c8c0061244 添加权限鉴定 2015-11-04 16:24:52 +08:00
ibuler
ad4b68a390 Merge branch 'jlog' of git.coding.net:jumpserver/Jumpserver into jlog 2015-11-04 15:21:44 +08:00
kelianchun_miller
a95e313733 update api.py 2015-11-04 15:20:28 +08:00
ibuler
8b25d3e32e Merge branch 'jlog' of git.coding.net:jumpserver/Jumpserver into jlog 2015-11-04 15:15:03 +08:00
ibuler
a0a7623743 修复log kill和在线查看命令统计 2015-11-04 15:11:14 +08:00
kelianchun_miller
5e20ac0454 update api.py 2015-11-04 14:56:50 +08:00
kelianchun_miller
d182736bd7 update api.py 2015-11-04 14:50:47 +08:00
root
159398b391 fix bugs 2015-11-03 23:53:12 +08:00
ibuler
e4ccb1c43f 修复日志打印的异常bug 2015-11-03 23:21:33 +08:00
ibuler
9d75ad6cea 多线程修复监控阻塞bug 2015-11-03 23:03:31 +08:00
ibuler
b30697ea20 基本完成 2015-11-03 19:12:15 +08:00
wangyong
0c0f05b6d9 jasset base 2015-11-03 17:07:46 +08:00
ibuler
50d21fcdca for log monitor 2015-11-02 23:20:40 +08:00
Zi Chuanxiu
c49a02d1c5 update ansible_api, and update perm_list_user view 2015-11-02 23:07:04 +08:00
Zi Chuanxiu
c26594a3ec update ansible_api, and update perm_list_user view 2015-11-02 23:05:19 +08:00
Zi Chuanxiu
6572e6f10e update ansible_api, and update perm_list_user view 2015-11-02 23:03:41 +08:00
liuzheng712
5144907ec0 there is no need to set the password value, very change user info needs password, make sure this is legal 2015-11-02 16:00:34 +08:00
liuzheng712
ba8ed23292 add the @login_required(login_url='/login') 2015-11-02 15:51:18 +08:00
liuzheng712
6583b0f530 change the login response redirect 2015-11-02 15:50:46 +08:00
liuzheng712
a3bea76994 juser/urls.py format 2015-11-02 15:49:05 +08:00
ibuler@qq.com
ebe8ce7109 some 2015-11-02 11:18:06 +08:00
Zi Chuanxiu
df9d879f22 ansible api base complete... 2015-10-31 11:55:19 +08:00
halcyon
46b1aca5f1 model 2015-10-31 09:23:10 +08:00
Zi Chuanxiu
dd4ac4e6d6 Task add method push_key 2015-10-30 23:14:45 +08:00
root
fe23e61036 Task add method push_key 2015-10-30 23:08:45 +08:00
root
b88c4211b2 update ansible_api.py 2015-10-30 08:25:22 +08:00
root
9e0d1a7285 add ansible_api module 2015-10-29 16:32:02 +08:00
广宏伟
73f94fecb5 fix import some bug 2015-10-29 12:01:03 +08:00
ibuler@qq.com
0e24ebdb26 merge with dev 2015-10-29 11:11:05 +08:00
广宏伟
d5bd2143e2 命令搜素完成 2015-10-28 22:32:16 +08:00
root
7d412e4c18 update configure file 2015-10-28 22:30:34 +08:00
liuzheng712
ac5ac5e06b change role finished , but need more think 2015-10-28 21:03:54 +08:00
ibuler@qq.com
53e4dc7acb search detail 2015-10-28 19:24:02 +08:00
liuzheng712
4d3e7d694b lots 2015-10-28 10:55:25 +08:00
liuzheng712
ca271900e6 remove request.session user_id and role_id, about role_id need more think, now is dirty 2015-10-27 23:57:37 +08:00
liuzheng712
f8c8c3deff update 2015-10-27 23:34:38 +08:00
ibuler@qq.com
76ad67307b search ok 2015-10-27 23:18:26 +08:00
ibuler@qq.com
ffab7ae697 some 2015-10-27 20:25:18 +08:00
wangyong
8909386d0c models 2015-10-27 20:13:23 +08:00
liuzheng712
8e9f22537f reuse asset_list for host_list 2015-10-27 17:40:53 +08:00
广宏伟
9366003f7b jlog 2015-10-26 22:17:16 +08:00
蓝枫
9018536651 Merge pull request #21 from shadowzey/master
页面错误修复
2015-10-26 11:42:08 +08:00
zyan
ec8ef4d1d7 Update idc_detail.html
修复IDC详情页面显示错误
2015-10-26 10:35:50 +08:00
ibuler@qq.com
72a18d6abf use map 2015-10-24 23:52:06 +08:00
ibuler@qq.com
79e81340dc modify log 2015-10-21 22:39:53 +08:00
ibuler@qq.com
4baffed481 base commit 2015-10-21 21:19:18 +08:00
广宏伟
77131a39d4 recovery playboo_run 2015-10-21 20:53:07 +08:00
广宏伟
40d00f7cbd lost playboo_run 2015-10-21 20:44:28 +08:00
ibuler@qq.com
fe1f825fdf perm edit 2015-10-19 23:40:16 +08:00
shadowzey
64973e43de 修复从查看部门页面进入主机列表点击详情404错误 2015-10-13 17:19:47 +08:00
蓝枫
e284584506 Merge pull request #20 from shadowzey/master
捕获添加用户邮箱填写错误时邮件发送失败的异常
2015-10-13 16:55:13 +08:00
严泽
0667067c0c 捕获添加用户邮箱填写错误时邮件发送失败的异常 2015-10-13 16:36:16 +08:00
ibuler@qq.com
50d2bfb272 someday 2015-10-12 09:58:38 +08:00
ibuler@qq.com
06eedff49f fix common 2015-10-07 20:14:07 +08:00
ibuler@qq.com
afbbad1604 授权管理 2015-10-07 17:16:20 +08:00
liuzheng712
9d5c4dbb33 juser/user_list change user password fix 2015-10-07 12:36:20 +08:00
liuzheng712
ce90472624 juser/user_list change user password fix 2015-10-07 12:31:40 +08:00
liuzheng712
85dcc8ba63 WARNINGS:
jlog.Log.pid: (fields.W122) 'max_length' is ignored when used with IntegerField
        HINT: Remove 'max_length' from field
2015-10-07 12:20:59 +08:00
liuzheng712
c9e192fae4 recovery User's function 2015-10-07 12:17:20 +08:00
liuzheng712
cee8b2d787 rollback jumpserver/views.py 54 : user = get_object() part and delete useless 'class CustomUser' 2015-10-07 12:01:14 +08:00
ibuler@qq.com
d3465be672 修改Ignore 2015-10-06 23:48:15 +08:00
ibuler@qq.com
e99b33c51e 修改使用新表结构 2015-10-06 23:47:53 +08:00
Administrator
43fe985143 添加组授权 2015-10-06 18:51:49 +08:00
Administrator
699046dad5 修改settings 2015-10-05 23:48:03 +08:00
guanghongwei
6482a5e214 add ignore 2015-10-04 21:51:23 +08:00
guanghongwei
fef3879c85 rm tmpfile 2015-10-04 21:50:51 +08:00
guanghongwei
9f7b066ca6 shouquanxiugai 2015-10-04 21:50:29 +08:00
liuzheng712
02218e624a lots 2015-10-04 19:07:42 +08:00
liuzheng712
5a5928483f lots 2015-10-04 00:53:01 +08:00
liuzheng712
11b3cee346 gitignore 2015-10-03 22:50:05 +08:00
蓝枫
949065be2c Update connect.py
修正vim颜色
2015-10-01 16:25:17 +08:00
蓝枫
6168ab270f Merge pull request #19 from IYism/master
bugfix:connect.py log_online.html
2015-09-25 17:20:48 +08:00
IYism
667862223c bugfix
修复chrome浏览器有时候不能阻断的bug
2015-09-25 16:33:11 +08:00
IYism
2f5cf429ed bugfix ip_list
bugfix ip_list
2015-09-25 16:30:15 +08:00
liuzheng712
7a94724dbc initial_data_test 2015-09-24 09:43:28 +08:00
liuzheng712
79c79432f5 usermodle 2015-09-15 23:38:06 +08:00
ibuler
d32ea9f9a1 授权管理 2015-09-11 00:04:07 +08:00
ibuler
907c2c7e97 资产修改基本完成 2015-09-10 22:51:42 +08:00
ibuler
960e45d0fa 添加asset_api 2015-09-09 00:19:36 +08:00
ibuler
8bf9103fcf 修改资产管理 2015-09-09 00:19:17 +08:00
ibuler
4eb78e151e 用户模块修改告一段落 2015-09-06 21:37:22 +08:00
ibuler
3efd810fe1 修改修改用户bug 2015-09-02 21:42:31 +08:00
ibuler
c21cdc2131 添加忘记密码 2015-08-31 23:04:53 +08:00
蓝枫
a4ac98332b Merge pull request #15 from xiaopeng163/master
Delete .DS_Store file
2015-08-31 10:32:06 +08:00
Peng Xiao
073e7b36fa Delete .DS_Store file
.DS_Store is the name of a file in the Apple OS X operating system for storing custom attributes of a folder such as the position of icons or the choice of a background image.
2015-08-31 10:28:54 +08:00
ibuler
44c69ded78 干掉ldap前 2015-08-30 14:03:10 +08:00
root
1e170714c0 修改用户添加视图 2015-08-29 00:09:36 +08:00
ibuler
f47cddbd5c 修改cpu占用问题 2015-08-28 10:04:07 +08:00
ibuler
1387e29591 解决cpu占用过大问题 2015-08-28 09:58:20 +08:00
ibuler
10d96a9767 修改用户组视图 2015-08-28 00:33:54 +08:00
ibuler
fd5041965b 分离juser方法 2015-08-27 00:08:45 +08:00
ibuler
33c624c76f 删除部门后 2015-08-27 00:08:20 +08:00
ibuler
913f93b91f 删除部门前 2015-08-26 23:31:32 +08:00
蓝枫
d34ec00a3a Merge pull request #14 from pengyao/master
exact match, thanks for pengyao
2015-08-26 16:12:36 +08:00
pengyao
e9c6924530 exact match 2015-08-26 16:06:22 +08:00
ibuler
c1facb939e 优化connect.py排序 2015-08-22 00:01:54 +08:00
ibuler
ad16d8b532 优化connect.py 和 api.py 2015-08-21 23:45:41 +08:00
Guang
9f0620f97e 调整User,UserGroup类 2015-08-20 23:42:27 +08:00
蓝枫
a040b1c181 Update connect.py
fix cpu used 100%
2015-07-21 10:26:49 +08:00
蓝枫
8121e4942b Merge pull request #13 from ljjjustin/master
fix error url in host search page
2015-07-08 19:20:00 +08:00
Jiajun Liu
f5e57da106 fix line break bug on add multiple hosts 2015-07-07 18:12:25 +08:00
Jiajun Liu
b8dfb26a3a fix error url in host search page 2015-07-07 17:44:12 +08:00
蓝枫
079338f89d Merge pull request #12 from ljjjustin/master
fix user can not get login logs bug
2015-07-07 17:13:18 +08:00
Jiajun Liu
627d627995 fix user can not get login logs bug 2015-07-07 16:59:24 +08:00
Jiajun Liu
19343540b9 fix line break bug
some system user '\n\r' as line break, if we split commands by '\n',
then '\r' will be considered as part of commands which will break sudo
authentication. we can use splitlines function to avoid the problem.
2015-07-07 16:59:24 +08:00
Jiajun Liu
915adb2fd3 fix user can not get permission applies bug 2015-07-07 16:59:21 +08:00
ibuler
c25cd3c005 Merge remote-tracking branch 'remotes/github/master' 2015-07-03 21:29:25 +08:00
ibuler
32ab051bbc modify cmd_group num bug 2015-07-03 21:27:20 +08:00
ibuler
f7e70e56e3 modify sudo conn timeout bug 2015-07-03 21:09:17 +08:00
ibuler
9e52e6a320 modify some 2015-07-03 20:45:45 +08:00
ibuler
f3a0c390b1 connect.py base ok 2015-06-16 09:21:11 +08:00
ibuler
bb32c0480c dev 2015-06-15 23:00:19 +08:00
ibuler
8d167baf46 modify some 2015-06-15 19:20:05 +08:00
蓝枫
acfa0171ca Update log_offline.html
a small bug
2015-06-15 16:06:12 +08:00
ibuler
fdcaa358e5 modify some api.py bug 2015-06-13 00:36:10 +08:00
ibuler
ce8e1e0ae3 Merge remote-tracking branch 'remotes/github/master' 2015-06-11 14:05:16 +08:00
ibuler
7cb4ff97ea modify cmd_edit bug 2015-06-11 14:01:49 +08:00
ibuler
5d16836b05 modify connect.py 2015-06-10 23:16:24 +08:00
ibuler
fc22677f3f modify some 2015-06-09 23:06:32 +08:00
蓝枫
787a2591c4 Update layer.min.js
update url
2015-06-09 16:30:59 +08:00
ibuler
95cc2830c8 resolve kill process bug 2015-06-09 09:49:31 +08:00
ibuler
5bae8245bd modify source ip bug 2015-06-09 09:32:35 +08:00
ibuler
e979c2753b modify 1 2015-06-08 23:46:40 +08:00
ibuler
dcfd4e05ec modify sudo cmd bug 2015-06-04 10:15:41 +08:00
ibuler
1662269153 websocket auth add 2015-05-28 17:00:03 +08:00
ibuler
ce99bf2266 placeholder 2015-05-25 18:35:32 +08:00
ibuler
1952d1ca23 modify sudo show 2015-05-25 18:27:57 +08:00
ibuler
bf80799fb6 modify port type 2015-05-25 17:13:08 +08:00
ibuler
38f89042ac modify single user login bug 2015-05-25 14:16:08 +08:00
ibuler
67b2467bc9 Merge branch 'guanghongwei' 2015-05-25 10:10:49 +08:00
ibuler
345be469fa Merge branch 'wangyong' 2015-05-25 10:07:25 +08:00
halcyon
04522c0b80 修改port字段 2015-05-25 10:03:55 +08:00
蓝枫
4a9e002440 Merge pull request #6 from losswei/master
修复当用户使用Ctrl+C结束进程时,没有正确退出
2015-05-22 17:24:46 +08:00
何威
6998036bdc 修复当用户使用Ctrl+C结束进程时,没有正确退出 2015-05-22 17:18:37 +08:00
ibuler
e09cb10439 modify a bug with 10.0.0.1 10.0.0.11 10.0.0.12 match problem 2015-05-18 17:22:26 +08:00
ibuler
caa318b808 modify a bug with 10.0.0.1 10.0.0.11 10.0.0.12 match problem 2015-05-18 16:40:36 +08:00
ibuler
58fd4374c7 Merge branch 'wangyong' 2015-05-15 12:01:51 +08:00
ibuler
f89d444ddf modify requirements.txt 2015-05-15 11:53:31 +08:00
root
49bee0c3fd 修改主机添加或者修改没添加进ALL组bug 2015-05-14 18:02:20 +08:00
ibuler
00f7346578 resolve connect.py string start with g bug 2015-04-28 16:54:58 +08:00
ibuler
b1add204b6 ޸ʾ 2015-04-28 15:08:28 +08:00
ibuler
6d34bd45d2 modify nav_bar_header.html 2015-04-24 20:42:15 +08:00
ibuler
992947ac99 modify service.sh bug 2015-04-24 20:39:44 +08:00
ibuler
ba55cdde87 modify service.sh bug 2015-04-24 19:12:14 +08:00
ibuler
c01a489dce modify service.sh bug 2015-04-24 18:19:43 +08:00
ibuler
0beb683c40 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-04-23 11:23:05 +08:00
root
b134c90bfd 解决阻断报错问题 2015-04-23 11:20:12 +08:00
ibuler
6c10cf1a5a Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-04-22 18:58:26 +08:00
ibuler
311b65da00 ޸bug 2015-04-22 18:57:55 +08:00
halcyon
8a8f68d908 使用username 去掉log_ha里的print 2015-04-22 18:04:23 +08:00
halcyon
b7aa1ab3dc jperm问题 2015-04-22 17:50:46 +08:00
halcyon
27d6df7a38 解决django低版本不支持first()问题 2015-04-21 14:45:38 +08:00
ibuler
8f7e1bfc38 modify readme 2015-04-20 22:55:26 +08:00
ibuler
7accf70e17 ӽű 2015-04-20 21:17:16 +08:00
ibuler
e2ac212fc3 resolve confilict 2015-04-20 20:56:26 +08:00
ibuler
09d3763a02 fix a bug 2015-04-20 20:54:10 +08:00
ibuler
ea37374ea5 django 1.6 2015-04-20 18:56:30 +08:00
ibuler
a203b89ad4 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-04-20 15:57:33 +08:00
ibuler
0a8198b2ca ޸Ƴbug 2015-04-20 15:57:10 +08:00
ibuler
d84ff97019 ޸Ƴbug 2015-04-20 15:56:49 +08:00
ibuler
412dadce76 version 2.0.0 beta 2015-04-20 10:12:11 +08:00
halcyon
b65f620ff7 hehe 2015-04-19 23:14:51 +08:00
ibuler
8b50e5252f merge with wangyong 2015-04-19 18:45:07 +08:00
ibuler
76b1bf2c39 Խűȥע 2015-04-19 18:39:15 +08:00
halcyon
86e334e4fa last bug? 2015-04-19 18:36:41 +08:00
ibuler
17e89bb55b ޸һbug 2015-04-19 18:26:38 +08:00
ibuler
d5fba23d73 ȨҳûҳӵȨб 2015-04-19 00:23:35 +08:00
ibuler
e96053ba20 ޸ 2015-04-18 19:15:53 +08:00
ibuler
c8d91884c8 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver
Conflicts:
	jumpserver/api.py
2015-04-18 18:06:17 +08:00
halcyon
bd2d7ce007 hehe 2015-04-18 18:04:03 +08:00
ibuler
6d6e9d97b5 ޸ 2015-04-18 18:04:01 +08:00
ibuler
9ab855fd92 merge 2015-04-18 17:00:03 +08:00
ibuler
c1d8de4558 ϲ 2015-04-18 16:58:16 +08:00
halcyon
f19b274fcc bug 2015-04-18 16:58:15 +08:00
ibuler
0873211d13 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-04-18 14:02:57 +08:00
ibuler
bcc5774365 ӳɹҳ 2015-04-18 13:52:28 +08:00
halcyon
84929840d0 bug 2015-04-18 13:51:47 +08:00
ibuler
d6171f7fe7 merge 2015-04-18 12:37:55 +08:00
ibuler
9acba2ddf0 ɾûõ 2015-04-18 12:36:16 +08:00
halcyon
75e1ea7f7d bug 2015-04-18 12:30:45 +08:00
ibuler
4f1b4f1947 ޸bug 2015-04-17 22:22:21 +08:00
halcyon
a269c43d1c 优化 2015-04-16 19:03:02 +08:00
ibuler
83938e10b2 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-04-16 15:13:45 +08:00
ibuler
0d1d64e83c ޸ļ 2015-04-16 15:13:22 +08:00
halcyon
75c8e44575 bugs 2015-04-16 15:10:46 +08:00
halcyon
29d196410e Merge branch 'master' into wangyong 2015-04-16 14:34:49 +08:00
ibuler
e2e131146d Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-04-16 14:33:55 +08:00
ibuler
60cb5561f0 ޸move 2015-04-16 14:32:51 +08:00
halcyon
55c5230eb2 hehe 2015-04-16 14:32:48 +08:00
halcyon
da4d6b85fc bugs 2015-04-15 18:47:02 +08:00
guanghongwei
c08cee8052 û 2015-04-15 17:32:30 +08:00
guanghongwei
3424bef5d0 ޸bug 2015-04-15 12:07:59 +08:00
halcyon
e72e30bc4e bug 2015-04-14 21:06:19 +08:00
halcyon
99293cc019 bug 2015-04-14 20:09:11 +08:00
halcyon
3bfe0e3c16 bugs 2015-04-14 17:25:56 +08:00
halcyon
4abf25ef13 hehe 2015-04-13 21:25:43 +08:00
halcyon
e385f3e09d Merge branch 'master' into wangyong 2015-04-11 19:57:20 +08:00
halcyon
a4bd352486 idc 2015-04-11 19:56:53 +08:00
guanghongwei
6a565d0a45 reslove 2015-04-11 17:49:41 +08:00
root
2e254af935 hehe 2015-04-11 17:45:49 +08:00
guanghongwei
12df8cf060 ޸ 2015-04-11 17:45:33 +08:00
guanghongwei
3c1af0e266 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-04-11 12:53:22 +08:00
guanghongwei
3b00cfa97d ޸ĸbug 2015-04-11 12:52:17 +08:00
halcyon
9cfe6c569d hehe 2015-04-11 12:52:00 +08:00
guanghongwei
78f00e5c5f ϴļ 2015-04-09 23:49:21 +08:00
guanghongwei
30fd160079 upload and resove 2015-04-09 13:55:16 +08:00
halcyon
3842f7795b hehe 2015-04-09 11:48:53 +08:00
guanghongwei
419ee2660a Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	jumpserver/urls.py
	jumpserver/views.py
2015-04-09 11:32:32 +08:00
guanghongwei
ff77cc28fa ϴ 2015-04-09 11:27:36 +08:00
halcyon
d28d72c42d bugs 2015-04-09 11:09:18 +08:00
guanghongwei
2b14fefb5c ޸ҳ 2015-04-08 18:08:39 +08:00
guanghongwei
25ce24a17d Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver
Conflicts:
	jasset/views.py
	jumpserver/views.py
2015-04-08 17:39:00 +08:00
guanghongwei
b840db12a6 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver
Conflicts:
	jasset/views.py
	jumpserver/views.py
2015-04-08 17:38:30 +08:00
guanghongwei
2035a49c40 ޸Կkey, ͨûҳ 2015-04-08 17:32:56 +08:00
halcyon
bb94cfc7a1 完善 2015-04-08 17:31:07 +08:00
halcyon
ef63fce7c4 修改bug,实时搜索完成 2015-04-07 19:15:45 +08:00
guanghongwei
bd0fd90e2d ûͨû 2015-04-06 23:04:31 +08:00
halcyon
7de943c6e4 管理员仪表盘基本完成 2015-04-05 22:52:37 +08:00
halcyon
17be0bb39e dashboard and api 2015-04-05 17:52:46 +08:00
guanghongwei
7e53228359 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-04-04 12:48:56 +08:00
guanghongwei
afcc784797 ޸û 2015-04-04 12:48:26 +08:00
halcyon
6e976b0583 0406 2015-04-04 12:47:07 +08:00
guanghongwei
8f66de365e merge with wangyong 2015-04-02 20:40:54 +08:00
halcyon
b35f1e6174 权限申请 2015-04-02 18:32:43 +08:00
guanghongwei
a998de5973 ޸û 2015-04-01 22:54:10 +08:00
halcyon
e1b9134d7f 0402 2015-04-01 18:58:10 +08:00
蓝枫
544bc10997 Update views.py
修改查看权限列表和添加权限列表不一致的bug
2015-03-31 21:07:50 +08:00
guanghongwei
2e5b22417d л 2015-03-30 21:31:02 +08:00
guanghongwei
38445a1dd2 Ұ 2015-03-27 18:37:10 +08:00
guanghongwei
340aaf42de ˢ 2015-03-27 16:34:00 +08:00
halcyon
359f70b9f9 apply 2015-03-26 18:42:52 +08:00
halcyon
408e4a54d8 bugs 2015-03-25 19:02:14 +08:00
guanghongwei
59414dadfc ˢsudosudoûȨ 2015-03-25 18:45:55 +08:00
halcyon
e3b2be0261 bugs 2015-03-24 18:34:00 +08:00
halcyon
6816f941d9 Merge branch 'guanghongwei' into wangyong 2015-03-24 17:05:17 +08:00
halcyon
1dddd98741 bug 2015-03-24 17:04:25 +08:00
guanghongwei
0c31968e3c 2015-03-23 22:57:19 +08:00
guanghongwei
2f7e7b0072 ޸nav.html 2015-03-21 15:08:16 +08:00
guanghongwei
d8a6eba2f3 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	connect.py
	jumpserver.conf
	jumpserver/api.py
2015-03-21 13:10:38 +08:00
guanghongwei
18b6e25cf3 ɾûõ 2015-03-21 13:00:12 +08:00
halcyon
df2c2fc7d5 zhoumo 2015-03-21 12:58:29 +08:00
guanghongwei
e717fad762 ȵ 2015-03-19 23:57:08 +08:00
halcyon
fef224459c 鉴权 2015-03-19 18:32:10 +08:00
halcyon
9348a0deb7 鉴权 2015-03-18 18:05:46 +08:00
root
fa0ec1e7d7 connect 2015-03-17 23:20:25 +08:00
halcyon
98dfebc3aa jlog 2015-03-17 17:18:41 +08:00
halcyon
92ccd3dc78 jlog 2015-03-17 16:25:32 +08:00
halcyon
a9a0b4ee49 bugs 2015-03-17 15:40:52 +08:00
halcyon
5668794171 bugs 2015-03-17 15:31:23 +08:00
halcyon
6759146426 bugs 2015-03-17 15:28:48 +08:00
halcyon
d24cfeeabc bugs 2015-03-17 14:22:35 +08:00
halcyon
a36c8599e7 bugs 2015-03-17 14:12:49 +08:00
halcyon
9e1dd270ae bugs 2015-03-17 13:56:43 +08:00
guanghongwei
386d6a1cb2 2015-03-16 22:38:42 +08:00
guanghongwei
a2ede31ae6 Ȩʼ 2015-03-15 23:23:20 +08:00
guanghongwei
f3ae7f00a0 ʱ 2015-03-14 23:57:53 +08:00
guanghongwei
56df1f6963 merge 2015-03-14 17:57:26 +08:00
halcyon
78dc6e5154 bugs 2015-03-14 17:54:27 +08:00
guanghongwei
026836ebc5 2015-03-14 17:54:17 +08:00
guanghongwei
d926cbdef2 merge with wangyong 2015-03-14 13:20:53 +08:00
guanghongwei
d1f19a5cad ޸IJʾ 2015-03-14 13:15:33 +08:00
halcyon
ce6494ba64 bugs 2015-03-14 13:13:46 +08:00
guanghongwei
b2c72221f7 2015-03-13 23:46:38 +08:00
guanghongwei
5749276812 Ȩ޹ 2015-03-13 18:36:35 +08:00
guanghongwei
a7800b9a7a Ȩ޹ 2015-03-13 00:09:18 +08:00
halcyon
691271c74f bugs 2015-03-12 18:54:26 +08:00
guanghongwei
475501595e ҳ 2015-03-12 18:43:17 +08:00
halcyon
fd490bd685 bugs 2015-03-12 11:03:53 +08:00
guanghongwei
546393d9c3 Ȩ޸ 2015-03-11 23:46:35 +08:00
halcyon
75f06f9b62 bugs 2015-03-11 18:29:47 +08:00
guanghongwei
80d4272ee5 Merge remote-tracking branch 'origin/guanghongwei' into guanghongwei
Conflicts:
	jperm/models.py
2015-03-11 18:00:55 +08:00
guanghongwei
ae0c28ef8f 2015-03-11 17:59:15 +08:00
guanghongwei
5c54128f8d ޸ĺܶ, 2015-03-11 10:18:20 +08:00
halcyon
10bcae7cf0 dashboard 2015-03-10 18:27:47 +08:00
guanghongwei
d72d55c965 没怎么修改 2015-03-08 22:48:50 +08:00
guanghongwei
6c10b06d5a Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-03-07 18:38:29 +08:00
halcyon
95bb96ebbf 各种bug修改 2015-03-07 18:37:23 +08:00
guanghongwei
8f163eb634 统一分页 2015-03-07 18:25:31 +08:00
guanghongwei
de358099cf Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-03-07 18:07:04 +08:00
guanghongwei
cbf8dac5cc 修改显示分页 2015-03-07 18:06:50 +08:00
halcyon
e81eefcf62 修改各种bug 2015-03-07 18:06:26 +08:00
guanghongwei
c0eec734a1 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-03-07 17:51:08 +08:00
halcyon
6c9c560b86 修改各种bug 2015-03-07 17:50:17 +08:00
guanghongwei
c9928fc1f9 Merge branches 'master' and 'wangyong' of gitcafe.com:ibuler/jumpserver
Conflicts:
	docs/AddUserAsset.py
2015-03-07 17:39:10 +08:00
halcyon
5bf66ca92e 修改各种bug 2015-03-07 17:37:40 +08:00
guanghongwei
9a638bc856 修改一些bug 2015-03-07 17:37:26 +08:00
guanghongwei
67f9aad1b9 Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	docs/AddUserAsset.py
2015-03-07 15:33:06 +08:00
halcyon
60729c49df bug 2015-03-07 15:29:49 +08:00
guanghongwei
ac92498832 修改 权限修改为 submit 2015-03-07 15:28:49 +08:00
guanghongwei
a94c050fd9 完工,睡觉 2015-03-07 00:12:38 +08:00
guanghongwei
07b0e2980e 今天到此为止 2015-03-06 00:24:17 +08:00
halcyon
d0600662ba dashboard and bugs 2015-03-05 18:46:12 +08:00
guanghongwei
8bc40dbea1 基本完成用户管理 2015-03-05 00:20:24 +08:00
root
e086a03692 bug 2015-03-04 23:14:07 +08:00
halcyon
bbe5b588c0 修改bug 2015-03-04 18:33:38 +08:00
guanghongwei
d32026013f ޸ĺܶ 2015-03-04 18:20:03 +08:00
guanghongwei
50fcac67c3 修改 删除等 2015-03-03 22:44:00 +08:00
guanghongwei
fc007e96d7 ޸ĺܶ 2015-03-03 18:42:06 +08:00
guanghongwei
ad2a4d2d5d 修改 用户添加 和 组添加 2015-03-02 23:32:34 +08:00
guanghongwei
d90b59191f ع1 2015-03-02 18:27:48 +08:00
guanghongwei
458ca42f22 未完成 2015-03-01 23:34:00 +08:00
guanghongwei
a50d3ba692 头像大小导致 nav颜色中间差 2015-03-01 22:46:19 +08:00
guanghongwei
fb1c17dc51 修改bug 2015-03-01 15:01:15 +08:00
guanghongwei
da1beef94d Merge branch 'guanghongwei'
Conflicts:
	jumpserver/views.py
	static/js/base.js
	templates/head_script.html
2015-03-01 12:26:52 +08:00
guanghongwei
32ab6311ae 合并分支 2015-03-01 12:23:26 +08:00
halcyon
08410820ab 03-01 2015-03-01 12:16:14 +08:00
halcyon
3c70512d8e dashboard 2015-03-01 12:09:33 +08:00
guanghongwei
1e26543f99 添加头像 2015-02-28 23:24:10 +08:00
guanghongwei
1c96443e73 修改头像 2015-02-28 23:22:52 +08:00
guanghongwei
9bab7bf1e7 Merge branch 'guanghongwei' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	jperm/views.py
	jumpserver/views.py
2015-02-28 21:26:23 +08:00
root
cf8af114fe dashboard 2015-02-27 23:18:40 +08:00
guanghongwei
f0cd064195 修改 用户左侧显示 2015-02-27 22:14:09 +08:00
halcyon
9d44bc1593 dashboard con 2015-02-27 18:48:24 +08:00
root
67c1f12086 dashboard 2015-02-26 18:02:39 +08:00
guanghongwei
56f8f5d09b ޸ķҳ 2015-02-15 10:06:02 +08:00
guanghongwei
b7acef8cb0 授权收尾工作 2015-02-13 00:00:07 +08:00
root
a746cab21c bug 2015-02-12 23:17:07 +08:00
halcyon
b1e6699330 每日一更? 2015-02-12 18:54:40 +08:00
guanghongwei
4190b19450 修改授权详情 2015-02-11 23:32:24 +08:00
guanghongwei
f050866d94 Ȩ 2015-02-11 18:38:56 +08:00
halcyon
db86daf86d dashboard等 2015-02-11 17:53:17 +08:00
guanghongwei
97038aab37 sudoȨ 2015-02-10 21:52:59 +08:00
halcyon
a42d053cbb 新弹窗等 2015-02-10 18:57:43 +08:00
guanghongwei
50208c0088 sudoȨ 2015-02-10 18:53:01 +08:00
guanghongwei
8a5e494cfe 添加sudo权限删除,修改和详情 2015-02-10 00:21:08 +08:00
halcyon
5f2e2a808e 添加搜索功能 2015-02-09 19:03:18 +08:00
halcyon
c84f124511 bug 2015-02-09 19:02:25 +08:00
guanghongwei
d7de3edcf4 sudo޸Ȩ 2015-02-09 18:50:21 +08:00
guanghongwei
b9e2c9aa95 修改sudo授权 2015-02-09 08:40:54 +08:00
guanghongwei
75979e3999 修改sudo授权 2015-02-07 00:07:07 +08:00
guanghongwei
c113035d3d sudo perm 2015-02-06 18:39:20 +08:00
halcyon
433974d77d 改bug 2015-02-05 18:53:57 +08:00
guanghongwei
121b76284a Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	static/js/base.js
	templates/head_script.html
2015-02-03 23:15:22 +08:00
guanghongwei
46cc593a9e Ȩ֧ 2015-02-03 23:03:51 +08:00
halcyon
de58e92df9 添加组修改,组内删除主机并不删除真实机 2015-02-03 22:45:51 +08:00
蓝枫
0acc90a691 修改用户分页的bug
修改用户分页的bug
2015-02-02 11:29:32 +08:00
guanghongwei
70fe42f359 ޸scriptλ 2015-02-01 23:33:38 +08:00
guanghongwei
3d1df73a99 merge with wangyong 2015-02-01 23:12:57 +08:00
guanghongwei
0336bfa906 group_addʹajax 2015-02-01 23:03:28 +08:00
halcyon
6c70d5bf03 批量添加删除功能实现,修改资产添加修改等 2015-02-01 23:00:23 +08:00
halcyon
0560332768 change view 2015-01-31 13:46:55 +08:00
guanghongwei
7adbf52aeb ޸jsλ 2015-01-31 13:45:58 +08:00
root
795183c7ab remove node module from repo 2015-01-31 13:09:29 +08:00
root
4e0cd4cddf modify ignore 2015-01-31 13:06:48 +08:00
root
f187a368e4 add ignore 2015-01-31 13:06:02 +08:00
root
2a645747de Merge in 20150131 2015-01-31 13:03:58 +08:00
guanghongwei
deb55d3bb2 logo 2015-01-31 12:56:32 +08:00
halcyon
a8991bfb14 资产修改功能强化,日志功能添加 2015-01-31 12:55:10 +08:00
guanghongwei
c1fb72f393 ޸ĵ½˳ 2015-01-30 23:30:16 +08:00
guanghongwei
35d9be5c10 connect.py޸ 2015-01-30 16:53:06 +08:00
guanghongwei
c1686d52e9 Ȩ༭Ͳ鿴޸ 2015-01-30 16:39:34 +08:00
guanghongwei
c09d1d25b2 Ȩ༭޸ok 2015-01-30 14:54:45 +08:00
guanghongwei
d25a3ce17c û޸ 2015-01-30 11:18:30 +08:00
蓝枫
263aeeee9b Merge pull request #2 from xxrenzhe/patch-1
Update runserver
2015-01-29 11:11:08 +08:00
guanghongwei
b4006d3d36 perm edit޸ 2015-01-29 00:53:15 +08:00
xxrenzhe
8c9b53419d Update runserver
add sudo to start runserver, avoid starting runserver failed
2015-01-28 19:04:48 +08:00
guanghongwei
b62bc9d161 û޸ĸһ 2015-01-28 18:00:09 +08:00
guanghongwei
6a8db89614 ޸һbug 2015-01-28 17:35:06 +08:00
guanghongwei
5cd09a6503 ޸ʱӳԱ 2015-01-28 15:08:16 +08:00
guanghongwei
5d38a1996c Կȥ 2015-01-28 11:17:29 +08:00
guanghongwei
2254e82bf0 ʾĬϵgroup 2015-01-27 18:53:37 +08:00
guanghongwei
308379406a ʾĬϵgroup 2015-01-27 18:05:53 +08:00
guanghongwei
c04237ac76 ʾĬϵgroup 2015-01-27 18:02:51 +08:00
guanghongwei
047dda5ab3 ʾĬϵgroup 2015-01-27 17:56:51 +08:00
guanghongwei
3c8a9bab0c ޸ɾû 2015-01-27 17:30:20 +08:00
guanghongwei
c782b46f32 û߼޸ 2015-01-27 17:24:33 +08:00
guanghongwei
2acc715ebe x޸js 2015-01-27 16:43:37 +08:00
guanghongwei
5ddcdf99c1 ޸Ϣ 2015-01-27 15:01:09 +08:00
ibuler
c4196e7d5a 修改组方面授权 2015-01-27 00:14:50 +08:00
halcyon
173f520929 添加idc和主机组合并 2015-01-26 23:32:52 +08:00
guanghongwei
ca3d4de819 ޸js bug 2015-01-26 11:51:12 +08:00
ibuler
c460323c5b 修改授权web
映射去掉 user_common 和 password_common ===
2015-01-25 23:38:58 +08:00
halcyon
d1f6ed4a55 增加日志查看功能 2015-01-25 22:20:07 +08:00
ibuler
569e12e83c 修改授权移动 2015-01-25 21:46:19 +08:00
ibuler
caff3e5d0c Merge branches 'guanghongwei' and 'master' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-01-25 12:20:57 +08:00
guanghongwei
20fb5d6699 ޸bug 2015-01-24 16:51:39 +08:00
guanghongwei
00ec6c9034 ˫ 2015-01-24 16:50:38 +08:00
guanghongwei
0b79f8e345 ˫ 2015-01-24 16:49:47 +08:00
guanghongwei
373e417729 ޸bug 2015-01-24 16:29:19 +08:00
guanghongwei
3903eedc1a ޸Ȩ 2015-01-24 16:27:29 +08:00
guanghongwei
de175eb3cd tab 2015-01-24 16:06:07 +08:00
guanghongwei
8738b517cc ޸bug 2015-01-24 15:07:14 +08:00
guanghongwei
34b78825d2 ޸Permssion --> Perm 2015-01-24 15:05:02 +08:00
guanghongwei
d7d988b067 ޸ 2015-01-24 15:04:02 +08:00
guanghongwei
ecbba5ea61 ޸perm 2015-01-24 15:02:50 +08:00
guanghongwei
90f078a0f8 ޸bug 2015-01-24 14:53:18 +08:00
guanghongwei
3f0e8fcc2d ޸tag perm_total -> perm_count 2015-01-24 14:50:21 +08:00
guanghongwei
aa7e8c0ed4 ޸tag perm_total -> perm_count 2015-01-24 14:49:38 +08:00
guanghongwei
df82c17c1c ޸nav perm_host 2015-01-24 14:48:29 +08:00
guanghongwei
96738976fb ޸jpermistion --> jperm 2015-01-24 14:46:35 +08:00
guanghongwei
1ea63bf3d0 ޸Ȩweb 2015-01-24 14:36:58 +08:00
root
3a9f0566b6 Merge branch 'wangyong' of gitcafe.com:ibuler/jumpserver 2015-01-24 13:58:31 +08:00
guanghongwei
81f80b4b0c Merge branches 'guanghongwei' and 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei 2015-01-24 13:55:07 +08:00
halcyon
1a266f0b19 菜单active bug 2015-01-24 13:54:08 +08:00
guanghongwei
95fbdce957 ޸bug 2015-01-24 13:49:52 +08:00
guanghongwei
7b0cea02bb ޸bug 2015-01-24 13:42:56 +08:00
guanghongwei
d2669c2de0 ޸group edit 2015-01-24 13:34:36 +08:00
guanghongwei
08d42f1c2c ޸user_edit group_edit 2015-01-24 13:20:52 +08:00
guanghongwei
b836a28e77 ޸bug 2015-01-24 12:55:20 +08:00
guanghongwei
6d545565f6 鿴޸ 2015-01-24 12:54:25 +08:00
halcyon
449bde4b03 菜单active 2015-01-24 12:13:49 +08:00
ibuler
dc8f6d4d01 修改bug 2015-01-24 00:20:29 +08:00
ibuler
ab4dab85cd 修改bug 2015-01-24 00:15:42 +08:00
ibuler
c55c77e349 修改bug 2015-01-23 23:45:55 +08:00
ibuler
54d659edb6 修改bug 2015-01-23 23:40:37 +08:00
ibuler
2de8930034 修改bug 2015-01-23 23:37:15 +08:00
ibuler
16a051f4a0 修改bug 2015-01-23 23:27:06 +08:00
ibuler
3dca51d23c 修改用户信息 2015-01-23 22:44:29 +08:00
ibuler
d5cf3935d8 修改bug 2015-01-23 00:46:35 +08:00
ibuler
327f3d6db1 修改bug 2015-01-23 00:45:45 +08:00
ibuler
49dcdf3be0 添加修改页面 2015-01-23 00:41:17 +08:00
ibuler
f951471f1d 修改bug 2015-01-23 00:08:39 +08:00
ibuler
6b2310ff38 添加删除用户 2015-01-23 00:07:44 +08:00
ibuler
7e41f28864 修改bug 2015-01-23 00:00:29 +08:00
ibuler
68f67aecce 添加显示详情 2015-01-22 23:57:18 +08:00
ibuler
c54bc85b5c 修改显示 2015-01-22 23:24:13 +08:00
ibuler
43cea075dd 修改bug 2015-01-22 23:15:50 +08:00
ibuler
be051afce9 修改对齐 2015-01-22 23:10:22 +08:00
ibuler
c25e05b746 修改bug 2015-01-22 23:05:25 +08:00
ibuler
8543e885b7 修改bug 2015-01-22 23:04:59 +08:00
halcyon
f335e35ba4 修改资产更改页面无法提交bug 2015-01-22 23:02:44 +08:00
ibuler
3cf352ee56 修改bug 2015-01-22 22:49:34 +08:00
ibuler
4f25124d92 修改分页 2015-01-22 22:46:48 +08:00
ibuler
cc72d59965 修改bug 2015-01-22 22:26:13 +08:00
ibuler
9dc7e953c9 修改错误 2015-01-22 22:16:32 +08:00
ibuler
984a04972f 修改错误 2015-01-22 22:14:21 +08:00
ibuler
cc425116a1 修改页面展示 2015-01-22 22:12:41 +08:00
ibuler
98061f9dce 修改bug 2015-01-22 00:17:12 +08:00
ibuler
81f7507bf2 添加查看授权页面 2015-01-22 00:16:28 +08:00
ibuler
736c58ce05 修改bug 2015-01-22 00:01:30 +08:00
ibuler
a8ce9e9952 添加授权 2015-01-21 23:56:18 +08:00
ibuler
8bc7485c88 修改bug 2015-01-21 23:46:51 +08:00
ibuler
393871e214 修改bug 2015-01-21 23:45:28 +08:00
ibuler
53c3767b1b 添加授权页面 2015-01-21 23:44:14 +08:00
ibuler
234f378e3f 修改bug 2015-01-21 23:28:56 +08:00
ibuler
5957286d57 查看授权用户 2015-01-21 23:27:56 +08:00
ibuler
f73f349153 修改nav.html 2015-01-21 23:23:44 +08:00
ibuler
59d034ce6e 添加查看权限用户 2015-01-21 23:22:21 +08:00
root
244d0d2ba4 Merge branch 'wangyong' of gitcafe.com:ibuler/jumpserver into guanghongwei
Conflicts:
	jumpserver/templatetags/mytags.py
	juser/views.py
2015-01-21 23:05:42 +08:00
ibuler
d878bb197f bug消除 2015-01-21 22:41:57 +08:00
ibuler
6bd0ed4344 修改bug 2015-01-21 22:40:50 +08:00
ibuler
6635ccb603 修改bug 2015-01-21 22:38:55 +08:00
ibuler
a0688274b6 查看实例 2015-01-21 22:33:31 +08:00
ibuler
c569afb5da 修改bug 2015-01-21 22:23:57 +08:00
ibuler
b01964618d 获得role tag 2015-01-21 22:23:05 +08:00
halcyon
9b194946c3 修改添加组不选没有提示bug 2015-01-21 18:54:31 +08:00
蓝枫
c3bbefc259 Update requirements.txt
pip install readline
2015-01-20 13:23:36 +08:00
halcyon
7863a6853c 资产管理页面基本完成,尚有部分小bug 2015-01-19 10:09:16 +08:00
蓝枫
cff27200fa Update jumpserver.py
update import readline
2015-01-18 21:33:37 +08:00
ibuler
46720dcf0f 修改bug 2015-01-14 23:58:25 +08:00
ibuler
f592e46442 添加用户列表 2015-01-14 23:51:30 +08:00
ibuler
2200783de7 添加用户列表 2015-01-14 23:30:20 +08:00
ibuler
8a1e5376f7 修改添加用户bug 2015-01-14 23:08:29 +08:00
halcyon
1925cf4033 增加js判断 2015-01-14 19:19:56 +08:00
halcyon
6b95c7ed6f 资产详情弹窗 2015-01-14 16:39:57 +08:00
ibuler
7435d602dd 修改添加用户bug 2015-01-13 23:15:40 +08:00
ibuler
2b6c2cd6fc 修改bug 2015-01-13 22:57:27 +08:00
ibuler
84a48d3aec 修改bug 2015-01-13 22:44:58 +08:00
ibuler
14da0f18ab 添加用户完成 2015-01-13 22:14:50 +08:00
ibuler
bc5b32bcea 添加useradd的函数 2015-01-12 23:52:41 +08:00
蓝枫
3f7f1a8cdb Update jumpserver.py
修改PS1显示
2015-01-12 13:31:20 +08:00
halcyon
acf508f074 资产管理页面基本完成. 2015-01-11 19:27:21 +08:00
ibuler
f45398e20b 修改默认 2015-01-10 23:39:43 +08:00
ibuler
9304a911ea 添加自定义tag 2015-01-10 23:31:32 +08:00
guanghongwei
fbde8d42fe ޸bug 2015-01-10 16:48:34 +08:00
guanghongwei
8875cb2f9e ޸bug 2015-01-10 16:28:09 +08:00
guanghongwei
8c9e763f74 Ĭֵ 2015-01-10 16:24:44 +08:00
guanghongwei
97dcb4ca91 Locals 2015-01-10 15:54:09 +08:00
guanghongwei
95fb1bfd1c ޸bug 2015-01-10 15:24:16 +08:00
guanghongwei
8b91d5774f ޸bug 2015-01-10 15:15:49 +08:00
guanghongwei
ae227aad77 ޸bug 2015-01-10 15:07:56 +08:00
guanghongwei
3a3c5701b7 ޸vbug 2015-01-10 15:04:34 +08:00
guanghongwei
56fe39d346 ޸vbug 2015-01-10 14:58:34 +08:00
guanghongwei
b3444d2398 ޸bug 2015-01-10 14:57:27 +08:00
guanghongwei
f7a54e13c3 ݿû 2015-01-10 14:52:35 +08:00
guanghongwei
796533b7a9 ޸bug 2015-01-10 14:07:06 +08:00
guanghongwei
f9abf7af1d Merge branch 'master' into guanghongwei 2015-01-10 14:00:02 +08:00
guanghongwei
e5fe13337b ޸jasset 2015-01-10 13:57:37 +08:00
root
94beae5282 a new branch 2015-01-10 13:52:39 +08:00
guanghongwei
65b29cb68d ޸ 2015-01-10 13:50:59 +08:00
guanghongwei
c6e0429584 ޸model 2015-01-10 13:35:56 +08:00
guanghongwei
30cfa8a5f4 ޸asset models 2015-01-10 13:30:01 +08:00
guanghongwei
67074fa895 ޸bug 2015-01-10 13:11:50 +08:00
guanghongwei
609b0160f7 ޸user_add.html 2015-01-10 13:10:44 +08:00
guanghongwei
0c30e8fd53 ѡ 2015-01-10 12:30:21 +08:00
guanghongwei
662c48ed37 group_list޸ 2015-01-10 12:07:15 +08:00
ibuler
a63f2304fe 修改icheck 2015-01-09 23:15:28 +08:00
ibuler
4af80d2b61 查看iCheck 2015-01-09 22:51:46 +08:00
ibuler
113054c92c 修改bug 2015-01-09 22:21:19 +08:00
ibuler
53d8f7fad7 修改checkbox样式 2015-01-09 22:19:05 +08:00
ibuler
fb525728c0 添加查看属组.html 2015-01-09 22:10:38 +08:00
ibuler
9e32964382 修改bug and 睡觉 2015-01-08 00:07:00 +08:00
ibuler
f524d2605a 修改 group_add bug 2015-01-08 00:01:41 +08:00
ibuler
fac7fe3281 添加 group add view 2015-01-07 23:56:36 +08:00
ibuler
2aea864fbe 添加group_add url 2015-01-07 23:22:47 +08:00
ibuler
9652460ba5 Merge remote-tracking branch 'origin/master' 2015-01-07 23:21:11 +08:00
ibuler
fde3d19a9d 添加分组template 2015-01-07 23:20:48 +08:00
halcyon
0ed0853489 完善资产添加和查看资产页面 2015-01-07 18:35:09 +08:00
ibuler
bafbacc18a 修改nav字体颜色 2015-01-06 23:45:40 +08:00
ibuler
cd7aa37a72 添加虚线 2015-01-06 23:36:11 +08:00
ibuler
e79aaff750 修改bug 2015-01-06 23:33:49 +08:00
ibuler
de226a9249 测试查看 user_add.html 2015-01-06 23:21:07 +08:00
ibuler
fc162f1429 测试查看 2015-01-06 23:10:13 +08:00
ibuler
6175f4eb29 长度再修改 2015-01-05 23:58:51 +08:00
ibuler
e74438f54d 修改长度 2015-01-05 23:57:49 +08:00
ibuler
64c155bede 修改添加用户 2015-01-05 23:55:05 +08:00
ibuler
c8828f40ba 测试修改Path 2015-01-05 23:52:25 +08:00
ibuler
ee3336de9e 修改长度 2015-01-05 23:46:13 +08:00
ibuler
f9a5907762 修改长度 2015-01-05 23:07:20 +08:00
ibuler
67c94265b1 修改长度 2015-01-05 23:03:46 +08:00
ibuler
ba704d61b6 修改长度 2015-01-05 22:57:36 +08:00
ibuler
a1973c2a89 修改长度 2015-01-05 22:51:15 +08:00
ibuler
3735136009 修改长度 2015-01-05 22:50:05 +08:00
ibuler
3ed3f3e830 修改长度 2015-01-05 22:45:10 +08:00
ibuler
3f54828114 修改title 2015-01-05 22:44:05 +08:00
ibuler
1c648d6396 添加 skin_config.html 2015-01-05 22:39:14 +08:00
ibuler
296302a236 添加/base/ 2015-01-05 22:37:03 +08:00
ibuler
f497003948 修改content 2015-01-05 22:35:25 +08:00
ibuler
ba5757e377 修改bug 2015-01-05 21:29:49 +08:00
ibuler
02ef9c687c Merge remote-tracking branch 'origin/master'
Conflicts:
	jumpserver/urls.py
	templates/nav.html
2015-01-05 21:25:14 +08:00
ibuler
a184ae9888 添加测试 useradd 2015-01-05 21:20:09 +08:00
halcyon
7a86ba0b51 资产管理页面开始,增加添加资产页面,修改base页面等 2015-01-05 18:21:34 +08:00
蓝枫
8ad37c6ce3 Update views.py
修改bug
2015-01-05 16:24:55 +08:00
halcyon
ccb4876d88 修改bug 2015-01-04 11:12:38 +08:00
ibuler
a4b3a07885 修改已出bug 2015-01-03 23:11:15 +08:00
ibuler
9087b41352 修改 文平提交 2015-01-03 23:10:09 +08:00
ibuler
8682d7c6ec 基于文平修改 2015-01-03 22:42:46 +08:00
ibuler
dde0b75d17 使用 陈文平的html 2015-01-03 22:24:34 +08:00
ibuler
a6642ab004 修改base.html 2015-01-03 20:42:20 +08:00
ibuler
caabc04ffe 添加支持readline 2015-01-03 09:52:52 +08:00
ibuler
f552babde6 添加facio 2015-01-02 22:08:49 +08:00
root
d51287fa8b 修改base.html 2015-01-02 22:06:35 +08:00
root
2cf845ceac Merge branch 'master' of gitcafe.com:ibuler/jumpserver 2015-01-02 20:53:31 +08:00
root
55c3b06d0f 简单定制base 2015-01-02 20:53:20 +08:00
ibuler
2a2e169da5 禁用google fonts 2015-01-02 20:52:52 +08:00
ibuler
6d32e6a1a7 添加Demo皮肤设置 2015-01-02 17:43:28 +08:00
ibuler
542873159b 添加Demo皮肤设置 2015-01-02 17:40:34 +08:00
ibuler
19a9e5aa2c 添加Demo皮肤设置 2015-01-02 17:26:11 +08:00
ibuler
9c1e313472 添加Demo皮肤设置 2015-01-02 17:24:56 +08:00
ibuler
d17c0bceb2 添加Demo皮肤设置 2015-01-02 17:23:59 +08:00
ibuler
8f95f11f35 添加Demo皮肤设置 2015-01-02 17:14:57 +08:00
ibuler
cd9a62f40b 删除没用的css 2015-01-02 16:59:37 +08:00
ibuler
bbc407660d 修改bug 2015-01-01 09:15:01 +08:00
ibuler
d1358b3d4f 修改bug 2015-01-01 08:44:46 +08:00
ibuler
2eaf6d7d81 修改bug 2014-12-31 23:02:03 +08:00
ibuler
73da287c6c 优化代码 2014-12-31 22:58:37 +08:00
ibuler
9ba7ad147a 修改multi拼写错误 2014-12-31 00:11:12 +08:00
root
b1e0a24184 Merge branch 'ssh_key'
Conflicts:
	connect.py
2014-12-30 23:59:27 +08:00
ibuler
894911126b 添加异常处理 2014-12-30 23:51:00 +08:00
ibuler
0b97c19862 修改bug 2014-12-30 23:22:57 +08:00
ibuler
89e2133341 serve active and user active 2014-12-30 22:52:39 +08:00
ibuler
3f4a7225b1 修改bug,增加ssh_key和普通密码登陆 2014-12-30 22:48:39 +08:00
halcyon
bd9049deb6 增加批量执行命令功能 2014-12-30 22:48:18 +08:00
ibuler
d316b08b6d 修改bug 2014-12-30 00:05:03 +08:00
ibuler
6c2b782964 支持ssh-key 2014-12-29 23:50:56 +08:00
ibuler
cefc175790 测试固定Footer 2014-12-29 21:26:09 +08:00
ibuler
d7df0fb48b 修改皮肤颜色 2014-12-29 21:23:34 +08:00
ibuler
c0d1ae6afc t图片bug 2014-12-29 21:20:10 +08:00
ibuler
c8c34dca29 修改地址 2014-12-29 21:16:43 +08:00
ibuler
588545c3f9 添加静态资源css,js 2014-12-28 21:52:23 +08:00
ibuler
7bfad09c49 修改bug 2014-12-28 21:49:35 +08:00
ibuler
2860f4a343 添加测试页面 2014-12-28 21:48:26 +08:00
ibuler
731196094b 添加解密时异常处理 2014-12-28 19:57:48 +08:00
root
c71e685913 修改.gitigonre 2014-12-28 00:42:21 +08:00
ibuler
fead462124 修改登录名和记录日志不一样的问题 2014-12-28 00:34:05 +08:00
ibuler
42d14ee1d6 修改bug 2014-12-28 00:14:10 +08:00
ibuler
1e78b65cd7 修改bug 2014-12-28 00:04:00 +08:00
root
37893bb394 修改配置文件 2014-12-27 23:57:40 +08:00
root
0744007701 添加测试 test.py 2014-12-27 23:56:16 +08:00
ibuler
8677b0d18d 修改bug 2014-12-27 23:54:37 +08:00
ibuler
7a73bd98d3 添加测试用户 2014-12-27 17:01:08 +08:00
ibuler
aa4c02cfcd 添加测试用户 2014-12-27 16:52:31 +08:00
ibuler
19bd6652af 修改bug 2014-12-26 23:59:12 +08:00
ibuler
0d48884e19 记录日志 2014-12-26 23:58:11 +08:00
ibuler
5e8605981b 增加开发者文档 2014-12-26 23:13:01 +08:00
ibuler
bcf3281a10 添加log models
添加 docs目录
2014-12-26 22:54:37 +08:00
root
37fe5dc4e3 add jlog 2014-12-26 21:57:10 +08:00
ibuler
9609bad31c 修改import 的序列 2014-12-26 21:55:56 +08:00
ibuler
f73ba9cc4a 显示id 2014-12-26 21:39:05 +08:00
ibuler
4eec31e42d 修改显示1 2014-12-26 00:11:13 +08:00
ibuler
8d7ab7b1b0 修改bug 2014-12-25 23:52:52 +08:00
root
284d5c79b0 chmod +x connect.py 2014-12-25 23:49:34 +08:00
ibuler
bc78331d5b 基本完成 2014-12-25 23:49:08 +08:00
ibuler
ab87420c9d 睡觉去了 2014-12-24 23:40:52 +08:00
ibuler
1f15f13aac 修改bUg 2014-12-24 23:17:51 +08:00
ibuler
fe70ca80e6 修改bUg 2014-12-24 22:56:29 +08:00
ibuler
17ef381d7c 改名,修改bug 2014-12-24 22:42:47 +08:00
ibuler
ac875d3961 简单修改 2014-12-24 22:29:36 +08:00
root
3c42240227 修改权限 2014-12-24 22:22:39 +08:00
ibuler
d7b2631410 修改名称 2014-12-24 22:21:46 +08:00
ibuler
e66b1b4192 修改bug 2014-12-24 22:20:38 +08:00
ibuler
803dc9c576 准备完工 2014-12-24 00:21:47 +08:00
root
5426116ed4 add a test file. 2014-12-23 22:48:43 +08:00
ibuler
c715c8ec02 修改bug, 添加测试文件 2014-12-23 22:27:45 +08:00
ibuler
a05e4147a3 修改bug 2014-12-22 23:46:00 +08:00
ibuler
0e76d52322 修改bug 2014-12-22 23:44:48 +08:00
ibuler
353bae0f13 修改bug 2014-12-22 23:43:03 +08:00
ibuler
39b4266458 修改bug 2014-12-22 23:42:11 +08:00
ibuler
bc5422821d 添加jpermssion models 2014-12-22 23:36:32 +08:00
ibuler
b24ad2854c 添加static, templates 2014-12-22 23:20:47 +08:00
root
a9af0598e7 add jpermssion app 2014-12-22 23:17:28 +08:00
ibuler
ef0c9fdd16 添加model 2014-12-22 23:14:29 +08:00
guanghongwei
99f95af74e modified settings.py set option 2014-12-22 18:34:08 +08:00
root
6a2e6709b9 add new app juser,jasset 2014-12-22 17:18:51 +08:00
guanghongwei
1e8d930ef0 del import module 2014-12-22 17:01:37 +08:00
guanghongwei
e4c6823ee2 alert_print fuction 2014-12-22 16:55:49 +08:00
guanghongwei
e2e32b4b37 add color print fuction 2014-12-22 16:38:10 +08:00
guanghongwei
7806db1d3f catch excepition 2014-12-22 16:22:04 +08:00
guanghongwei
3382b67bb8 Merge branch 'master' of gitcafe.com:ibuler/jumpserver 2014-12-22 15:58:07 +08:00
guanghongwei
b6866ac518 overite 2014-12-22 15:57:26 +08:00
root
f06422d5fa modify PS1 2014-12-22 15:56:19 +08:00
guanghongwei
e0a0c1c641 Merge branch 'master' of gitcafe.com:ibuler/jumpserver 2014-12-22 15:32:25 +08:00
guanghongwei
8baff2c72d set PS1 2014-12-22 15:31:48 +08:00
root
4805d18fe9 modify connect.py mode 2014-12-22 14:54:43 +08:00
guanghongwei
988991a0ad rename jumpserver.py --> connect.py and add connect fucntion 2014-12-22 14:52:31 +08:00
ibuler
917a24e858 Add postfix_shell function. Add jumpserver.conf file. 2014-12-21 23:10:59 +08:00
root
0b2a77fce4 Init project add LICENSE file 2014-12-21 22:13:04 +08:00
ibuler
23d2b2a1cf Initial commit 2014-12-21 22:04:16 +08:00
476 changed files with 80872 additions and 15664 deletions

45
.gitignore vendored
View File

@@ -1,3 +1,46 @@
*.py[cod]
.idea
*.pyc
test.py
.DS_Store
db.sqlite3
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
.settings
*.log
logs/*
keys/*
jumpserver.conf
nohup.out
tmp/*

339
LICENSE Normal file
View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
{description}
Copyright (C) {year} {fullname}
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
{signature of Ty Coon}, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

113
README.md
View File

@@ -1,39 +1,80 @@
Jumpserver是什么
-----------------
Jumpserver是用Python+Django写的一个开源的跳板机(堡垒机)项目,来集中管理服务器的账号、密码和权限。
## 写在前面
- 版本号变更 2.0 -> 0.2版本 3.0 -> 0.3版本
#欢迎使用Jumpserver
**Jumpserver** 是一款由python编写开源的跳板机(堡垒机)系统实现了跳板机应有的功能。基于ssh协议来管理客户端无需安装agent。
支持常见系统:
1. redhat centos
2. debian
3. suse ubuntu
4. freebsd
5. 其他ssh协议硬件设备
###截图:
首页
![webterminal](https://github.com/ibuler/static/raw/master/jumpserver3/index.jpg)
WebTerminal:
![webterminal](https://github.com/ibuler/static/raw/master/jumpserver3/webTerminal.gif)
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.jpg)
### 文档
* [访问wiki](https://github.com/jumpserver/jumpserver/wiki)
* [概览](https://github.com/jumpserver/jumpserver/wiki/%E6%A6%82%E8%A7%88)
* [名词解释](https://github.com/jumpserver/jumpserver/wiki/%E5%90%8D%E8%AF%8D%E8%A7%A3%E9%87%8A)
* [常见问题](https://github.com/jumpserver/jumpserver/wiki/%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98)
* 安装基于:[RedHat 的系统](https://github.com/jumpserver/jumpserver/wiki/%E5%9F%BA%E4%BA%8E-RedHat-%E7%9A%84%E7%B3%BB%E7%BB%9F)[Debian 的系统](https://github.com/jumpserver/jumpserver/wiki/%E5%9F%BA%E4%BA%8E-Debian-%E7%9A%84%E7%B3%BB%E7%BB%9F)
* [快速开始](https://github.com/jumpserver/jumpserver/wiki/%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
* [安装图解](https://github.com/jumpserver/jumpserver/wiki/%E5%AE%89%E8%A3%85%E5%9B%BE%E8%A7%A3)
* [应用图解](https://github.com/jumpserver/jumpserver/wiki/%E5%BA%94%E7%94%A8%E5%9B%BE%E8%A7%A3)
### 特点
* 完全开源GPL授权
* Python编写容易再次开发
* 实现了跳板机基本功能,认证、授权、审计
* 集成了Ansible批量命令等
* 支持WebTerminal
* Bootstrap编写界面美观
* 自动收集硬件信息
* 录像回放
* 命令搜索
* 实时监控
* 批量上传下载
### 其它
[Jumpserver官网](http://www.jumpserver.org)
[论坛](http://bbs.jumpserver.org)
[demo站点](http://demo.jumpserver.org)
交流群: 399218702
### 团队
![](https://github.com/ibuler/static/raw/master/jumpserver3/team.jpg)
1.1版本更新
-----------------
更新Log见笔者blog<br>
http://laoguang.blog.51cto.com/6013350/1576502
部署文档
-----------------
部署文档见笔者blog<br>
http://laoguang.blog.51cto.com/6013350/1576729
功能截图
-----------------
1. 安装<br>
![install](https://github.com/ibuler/static/blob/master/jumpserver1.1/1.%20install.png)
2. 登陆<br>
![login](https://github.com/ibuler/static/blob/master/jumpserver1.1/2.login.png)
3. 添加组<br>
![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/3.addgroup.png)
4. 添加用户<br>![adduser](https://github.com/ibuler/static/blob/master/jumpserver1.1/5.adduser.png)
5. 添加主机<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/9.addhost.png)
6. 添加权限<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/11.addperm.png)
7. 查看权限<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/13.showperm.png)
8. 下载私钥<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/16.downkey.png)
9. 登陆shell<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/17.loginshell.png)
10. 显示有权限的主机<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/18.p.png)
11. 批量执行命令<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/19.e.png)
12. 登陆有权限的主机<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/20.%20loginserver.png)
13. 查看sudo<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/22.showsudo.png)
14. 修改sudo<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/25.addsudohost.png)
15. 查看实时监控<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/29.monitor1ok.png)
16. 结束会话<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/28.%20killsession.png)
17. 查看统计日志<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/30.viewlog2.png)
18. 修改密码<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/34.modpass.png)
19. 下载文件<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/32.downfile.png)
20. 上传文件<br>![addgroup](https://github.com/ibuler/static/blob/master/jumpserver1.1/30.upfile.png)

779
connect.py Executable file
View File

@@ -0,0 +1,779 @@
#!/usr/bin/env python
# coding: utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf8')
import os
import re
import time
import datetime
import textwrap
import getpass
import readline
import django
import paramiko
import errno
import pyte
import operator
import struct, fcntl, signal, socket, select
from io import open as copen
import uuid
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
if not django.get_version().startswith('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
from jlog.views import TermLogRecorder
login_user = get_object(User, username=getpass.getuser())
try:
remote_ip = os.environ.get('SSH_CLIENT').split()[0]
except (IndexError, AttributeError):
remote_ip = os.popen("who -m | awk '{ print $NF }'").read().strip('()\n')
try:
import termios
import tty
except ImportError:
print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m'
time.sleep(3)
sys.exit()
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',
'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 write_log(f, msg):
msg = re.sub(r'[\r\n]', '\r\n', msg)
f.write(msg)
f.flush()
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.vim_end_flag = False
self.vim_end_pattern = re.compile(r'\x1b\[\?1049', re.X)
self.vim_pattern = re.compile(r'\W?vi[m]?\s.* | \W?fg\s.*', re.X)
self.vim_data = ''
self.stream = None
self.screen = None
self.__init_screen_stream()
def __init_screen_stream(self):
"""
初始化虚拟屏幕和字符流
"""
self.stream = pyte.ByteStream()
self.screen = pyte.Screen(80, 24)
self.stream.attach(self.screen)
@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 command_parser(self, command):
"""
处理命令中如果有ps1或者mysql的特殊情况,极端情况下会有ps1和mysql
:param command:要处理的字符传
:return:返回去除PS1或者mysql字符串的结果
"""
result = None
match = re.compile('\[?.*@.*\]?[\$#]\s').split(command)
if match:
# 只需要最后的一个PS1后面的字符串
result = match[-1].strip()
else:
# PS1没找到,查找mysql
match = re.split('mysql>\s', command)
if match:
# 只需要最后一个mysql后面的字符串
result = match[-1].strip()
return result
def deal_command(self, data):
"""
处理截获的命令
:param data: 要处理的命令
:return:返回最后的处理结果
"""
command = ''
try:
self.stream.feed(data)
# 从虚拟屏幕中获取处理后的数据
for line in reversed(self.screen.buffer):
line_data = "".join(map(operator.attrgetter("data"), line)).strip()
if len(line_data) > 0:
parser_result = self.command_parser(line_data)
if parser_result is not None:
# 2个条件写一起会有错误的数据
if len(parser_result) > 0:
command = parser_result
else:
command = line_data
break
if command != '':
# 判断用户输入的是否是vim 或者fg命令
if self.vim_pattern.search(command):
self.vim_flag = True
# 虚拟屏幕清空
self.screen.reset()
except Exception:
pass
return 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))
try:
mkdir(os.path.dirname(today_connect_log_dir), mode=0777)
mkdir(today_connect_log_dir, mode=0777)
except OSError:
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_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)
if self.login_type == 'ssh': # 如果是ssh连接过来记录connect.py的pidweb 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()
log_file_f.write('Start at %s\r\n' % datetime.datetime.now())
return log_file_f, log_time_f, log
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
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:
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
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)
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:
self.ssh = ssh
return ssh
class SshTty(Tty):
"""
A virtual tty class
一个虚拟终端类实现连接ssh和记录日志
"""
@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]
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
def posix_shell(self):
"""
Use paramiko channel connect server interactive.
使用paramiko模块的channel连接后端进入交互式
"""
log_file_f, log_time_f, log = self.get_log()
termlog = TermLogRecorder(User.objects.get(id=self.user.id))
termlog.setid(log.id)
old_tty = termios.tcgetattr(sys.stdin)
pre_timestamp = time.time()
data = ''
input_str = ''
input_mode = False
try:
tty.setraw(sys.stdin.fileno())
tty.setcbreak(sys.stdin.fileno())
self.channel.settimeout(0.0)
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
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
now_timestamp = time.time()
termlog.write(x)
termlog.recoder = False
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
input_str = ''
except socket.timeout:
pass
if sys.stdin in r:
try:
x = os.read(sys.stdin.fileno(), 4096)
except OSError:
pass
termlog.recoder = True
input_mode = True
input_str += x
if str(x) in ['\r', '\n', '\r\n']:
# 这个是用来处理用户的复制操作
if input_str != x:
data += input_str
if self.vim_flag:
match = self.vim_end_pattern.findall(self.vim_data)
if match:
if self.vim_end_flag or len(match) == 2:
self.vim_flag = False
self.vim_end_flag = False
else:
self.vim_end_flag = True
else:
data = self.deal_command(data)[0:200]
if len(data) > 0:
TtyLog(log=log, datetime=datetime.datetime.now(), cmd=data).save()
data = ''
input_str = ''
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()
termlog.save()
log.filename = termlog.filename
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()
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
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)
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'))
# 获取资产组包含的资产
asset_group = get_object(AssetGroup, id=gid)
if asset_group:
user_asset_search = asset_group.asset_set.all()
else:
color_print('没有该资产组或没有权限')
return
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:
# 如果没有输入就展现所有
user_asset_search = user_asset_all
self.search_result = dict(zip(range(len(user_asset_search)), user_asset_search))
color_print('[%-3s] %-12s %-15s %-5s %-10s %s' % ('ID', '主机名', 'IP', '端口', '系统用户', '备注'), '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:
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]
else:
color_print('当前用户未被授予角色,无法执行任何操作,如有疑问请联系管理员。')
return
assets = list(self.user_perm.get('role', {}).get(role).get('asset')) # 获取该用户,角色授权主机
print "授权包含该系统用户的所有主机"
for asset in assets:
print ' %s' % asset.hostname
print
print "请输入主机名或ansible支持的pattern, 多个主机:分隔, q退出"
pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip()
if pattern == 'q':
break
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
elif not command:
color_print('命令不能为空...')
continue
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 "请输入主机名或ansible支持的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'), 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 "请输入主机名或ansible支持的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:
color_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
def main():
"""
he he
主程序
"""
if not login_user: # 判断用户是否存在
color_print('没有该用户或许你是以root运行的 No that user.', exits=True)
if not login_user.is_active:
color_print('您的用户已禁用,请联系管理员.', 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 ID>:\033[0m ").strip()
except EOFError:
nav.print_nav()
continue
except KeyboardInterrupt:
sys.exit(0)
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']:
nav.print_asset_group()
continue
elif option in ['E', 'e']:
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:
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, e:
color_print(e)
time.sleep(5)
if __name__ == '__main__':
main()

127
docs/README.md Normal file
View File

@@ -0,0 +1,127 @@
快速安装
------
####环境
CentOS 6.x x86_64
iptables stop
selinux disable
####开始
**1. 安装git**
> yum -y install git
**2. 下载jumpserver**
> git clone https://github.com/ibuler/jumpserver.git
**3. 执行快速安装脚本**
> cd jumpserver/install && python install.py
*根据提示输入相关信息完成安装完成安装后请访问web继续查看后续文档*
名词解释
------
* **用户** 用户是授权和登陆的主体,将来为每个员工建立一个账户,用来登录跳板机,
将资产授权给该用户,查看用户登陆记录命令历史等
* **用户组** 多个用户可以组合成用户组,为了方便进行授权,可以将一个部门或几个用户
组建成用户组,在授权中使用组授权,该组中的用户拥有所有授权的主机权限
* **资产** 资产通常是我们的服务器、网络设备等,将资产授权给用户,用户则会有权限登
录资产,执行命令等
* **管理账户** 添加资产时需要添加一个管理账户,该账户是该资产上已有的有管理权限的用户,
如root或者有 NOPASSWD: ALL sudo权限的用户该管理账户用来向资产推送系统用户
为系统用户添加sudo获取资产的一些硬件信息
* **资产组** 同用户组,是资产组成的集合,为了方便授权
* **机房** 又称IDC不解释
* **Sudo** 这里的sudo其实是Linux中的sudo命令别名一个sudo别名包含多个命令
系统用户关联sudo就代表该系统用户有权限sudo执行这些命令
* **系统用户** 系统用户是服务器上建立的一些真实存在的可以ssh登陆的用户,如 dev,
sa, dba等系统用户可使用jumpserver推送到服务器上也可以利用自己公司
的工具进行推送,授权时将用户、资产、系统用户关联起来则表明用户有权限登陆该资产的
这个系统用户 如:用户 **小明****dev** 系统用户登陆 **172.16.1.1**资产
* **授权规则** 授权规则是将 **资产** **系统用户****用户** 关联起来,用来完成授权。
这样用户就可以以某个系统用户账号登陆资产
* **日志审计**
* **在线** 查看当前在线的用户(非web在线),可以监控用户的命令执行,强制结束用户
登录。
* **登录历史** 查看以往用户的登录历史,可以查看用户登陆操作的命令,可以回放用户
执行命令的录像
* **命令记录** 查看用户批量执行命令的历史,包含执行命令的主机,执行的命令,执行的结果
* **上传下载** 查看用户上传下载文件的记录
快速开始
------
##### 1. 添加用户
**用户管理 - 查看用户 - 添加用户** 填写基本信息,完成用户添加
用户添加完成后根据提示记住用户账号密码换个浏览器登录下载key
ssh登录jumpserver测试
##### 2. 添加资产
**资产管理 - 查看资产 - 添加资产** 填写基本信息,完成资产添加
##### 3. 添加sudo
**授权管理 - Sudo - 添加别名** 输入别名名称和命令完成sudo添加
##### 4. 添加系统用户
**授权管理 - 系统用户 - 添加** 输入基本信息,完成系统用户添加
##### 5. 推送系统用户
**授权管理 - 推送** - 选择需要推送的资产或资产组完成推送
推送只支持服务器使用密钥是指用户从跳板机跳转时使用key反之使用密码
授权时会检查推送记录,如果没有推送过则无法完成系统用户在该资产上的授权。
如果资产时网络设备,请不要选择密码和秘钥,模拟一下推送,目的是为了生成
推送记录。
##### 6. 添加授权规则
**授权管理 - 授权规则 - 添加规则** 选择刚才添加的用户,资产,系统用户完成授权
##### 7. 测试登录
**用户下载key** 登录跳板机会自动运行connect.py根据提示登录服务器
**用户登陆web** 查看授权的主机,点击后面的链接,测试是否可以登录服务器
##### 8. 监控和结束会话
**日志审计 - 在线** 查看当前登录的用户登录情况,点击监控查看用户执行的命令,
点击阻断,结束用户的会话
##### 9. 查看历史记录
**日志审计 - 登录历史** 查看登录历史,点击统计查看命令历史,点击回放查看录像
##### 10. 执行命令
同7 测试命令的执行,命令记录查看 批量执行命令的日志
##### 11. 上传下载
同7 测试文件的上传下载,日志审计 - 上传下载 查看上传下载记录

View File

@@ -0,0 +1,75 @@
# 使用Nginx搭建SSL配置
跳板机是所有服务器的入口,所以,它的安全至关重要。因此,建议把`Jumpserver`搭建在内网环境中并且加上SSL证书保证数据传输的安全。
## nginx的安装
不同的操作系统及版本,安装方法都不太一样。我们以`Debian`为例。
```
apt-get update
apt-get install -y nginx
```
更多安装示例请参考 [Nginx官方安装指南](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/)
## Nginx中的SSL的配置
* 编辑 `/etc/nginx/sites-enabled/default` 或者指定的`Jumpserver`的配置文件
* 示例如下
```
server {
listen 443;
listen 80;
server_name YOUR_DOMAIN;
ssl_certificate YOUR_DOMAIN_CRT;
ssl_certificate_key YOUR_DOMAIN_KEY;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
ssl on ;
if ($ssl_protocol = "") {
rewrite ^ https://$host$request_uri? permanent;
}
location / {
proxy_set_header Connection "";
proxy_http_version 1.1;
proxy_pass http://JUMPSERVER_HOST:WEB_PORT;
}
location /_ws/ {
keepalive_timeout 600s;
send_timeout 600s;
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
rewrite ^/_ws(/.*)$ $1 break;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://JUMPSERVER_HOST:WS_PORT;
}
}
```
* 请替换如下表格的关键字
关键字 | 示例 | 说明
------------- | ------------- |-------
`YOUR_DOMAIN` | example.com | `Jumpserver`的域名
`YOUR_DOMAIN_CRT` | /etc/nginx/certs/example.crt | SSL证书的CRT文件
`YOUR_DOMAIN_KEY` | /etc/nginx/certs/example.key | SSL证书的KEY文件
`JUMPSERVER_HOST` | 127.0.0.1 | `Jumpserver`服务器IP
`WEB_PORT ` | 80 | `Jumpserver`网页监听端口
`WS_PORT ` | 3000 | websocket端口`Jumpserver` 默认为3000
* 此配置会强制使用`https`, 建议加上(即if判断的那三行)。

10
init.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
#
trap '' SIGINT
base_dir=$(dirname $0)
export LANG='zh_CN.UTF-8'
python $base_dir/connect.py
exit

36
install/developer_doc.txt Normal file
View File

@@ -0,0 +1,36 @@
# 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
输入部分IPverify_connect匹配该部分ip,如果是匹配到多个就显示ip
匹配到0了就显示没有权限或者主机
匹配到1个则继续
查询该服务器是否支持ldap 如果是获得ldap用户密码登陆
如果否,查询授权表,查看该服务器授权的系统用户,并返回对应账号密码,登陆
connect函数是登陆函数采用paramiko 使用channel登陆posix_shell 来完成交互,并记录日志
signal模块来完成窗口改变导致的tty大小随之改变
PyCrypt是对称加密类

594
install/functions Normal file
View File

@@ -0,0 +1,594 @@
# -*-Shell-script-*-
#
# functions This file contains functions to be used by most or all
# shell scripts in the /etc/init.d directory.
#
TEXTDOMAIN=initscripts
# Make sure umask is sane
umask 022
# Set up a default search path.
PATH="/sbin:/usr/sbin:/bin:/usr/bin"
export PATH
if [ $PPID -ne 1 -a -z "$SYSTEMCTL_SKIP_REDIRECT" ] && \
( /bin/mountpoint -q /cgroup/systemd || /bin/mountpoint -q /sys/fs/cgroup/systemd ) ; then
case "$0" in
/etc/init.d/*|/etc/rc.d/init.d/*)
_use_systemctl=1
;;
esac
fi
systemctl_redirect () {
local s
local prog=${1##*/}
local command=$2
local options=""
case "$command" in
start)
s=$"Starting $prog (via systemctl): "
;;
stop)
s=$"Stopping $prog (via systemctl): "
;;
reload|try-reload)
s=$"Reloading $prog configuration (via systemctl): "
;;
restart|try-restart|condrestart)
s=$"Restarting $prog (via systemctl): "
;;
esac
if [ -n "$SYSTEMCTL_IGNORE_DEPENDENCIES" ] ; then
options="--ignore-dependencies"
fi
action "$s" /bin/systemctl $options $command "$prog.service"
}
# Get a sane screen width
[ -z "${COLUMNS:-}" ] && COLUMNS=80
if [ -z "${CONSOLETYPE:-}" ]; then
if [ -c "/dev/stderr" -a -r "/dev/stderr" ]; then
CONSOLETYPE="$(/sbin/consoletype < /dev/stderr 2>/dev/null)"
else
CONSOLETYPE="serial"
fi
fi
if [ -z "${NOLOCALE:-}" ] && [ -z "${LANGSH_SOURCED:-}" ] && [ -f /etc/sysconfig/i18n -o -f /etc/locale.conf ] ; then
. /etc/profile.d/lang.sh 2>/dev/null
# avoid propagating LANGSH_SOURCED any further
unset LANGSH_SOURCED
fi
# Read in our configuration
if [ -z "${BOOTUP:-}" ]; then
if [ -f /etc/sysconfig/init ]; then
. /etc/sysconfig/init
else
# This all seem confusing? Look in /etc/sysconfig/init,
# or in /usr/share/doc/initscripts-*/sysconfig.txt
BOOTUP=color
RES_COL=60
MOVE_TO_COL="echo -en \\033[${RES_COL}G"
SETCOLOR_SUCCESS="echo -en \\033[1;32m"
SETCOLOR_FAILURE="echo -en \\033[1;31m"
SETCOLOR_WARNING="echo -en \\033[1;33m"
SETCOLOR_NORMAL="echo -en \\033[0;39m"
LOGLEVEL=1
fi
if [ "$CONSOLETYPE" = "serial" ]; then
BOOTUP=serial
MOVE_TO_COL=
SETCOLOR_SUCCESS=
SETCOLOR_FAILURE=
SETCOLOR_WARNING=
SETCOLOR_NORMAL=
fi
fi
# Check if any of $pid (could be plural) are running
checkpid() {
local i
for i in $* ; do
[ -d "/proc/$i" ] && return 0
done
return 1
}
# __proc_pids {program} [pidfile]
# Set $pid to pids from /var/run* for {program}. $pid should be declared
# local in the caller.
# Returns LSB exit code for the 'status' action.
__pids_var_run() {
local base=${1##*/}
local pid_file=${2:-/var/run/$base.pid}
pid=
if [ -f "$pid_file" ] ; then
local line p
[ ! -r "$pid_file" ] && return 4 # "user had insufficient privilege"
while : ; do
read line
[ -z "$line" ] && break
for p in $line ; do
[ -z "${p//[0-9]/}" ] && [ -d "/proc/$p" ] && pid="$pid $p"
done
done < "$pid_file"
if [ -n "$pid" ]; then
return 0
fi
return 1 # "Program is dead and /var/run pid file exists"
fi
return 3 # "Program is not running"
}
# Output PIDs of matching processes, found using pidof
__pids_pidof() {
pidof -c -m -o $$ -o $PPID -o %PPID -x "$1" || \
pidof -c -m -o $$ -o $PPID -o %PPID -x "${1##*/}"
}
# A function to start a program.
daemon() {
# Test syntax.
local gotbase= force= nicelevel corelimit
local pid base= user= nice= bg= pid_file=
local cgroup=
nicelevel=0
while [ "$1" != "${1##[-+]}" ]; do
case $1 in
'') echo $"$0: Usage: daemon [+/-nicelevel] {program}"
return 1;;
--check)
base=$2
gotbase="yes"
shift 2
;;
--check=?*)
base=${1#--check=}
gotbase="yes"
shift
;;
--user)
user=$2
shift 2
;;
--user=?*)
user=${1#--user=}
shift
;;
--pidfile)
pid_file=$2
shift 2
;;
--pidfile=?*)
pid_file=${1#--pidfile=}
shift
;;
--force)
force="force"
shift
;;
[-+][0-9]*)
nice="nice -n $1"
shift
;;
*) echo $"$0: Usage: daemon [+/-nicelevel] {program}"
return 1;;
esac
done
# Save basename.
[ -z "$gotbase" ] && base=${1##*/}
# See if it's already running. Look *only* at the pid file.
__pids_var_run "$base" "$pid_file"
[ -n "$pid" -a -z "$force" ] && return
# make sure it doesn't core dump anywhere unless requested
corelimit="ulimit -S -c ${DAEMON_COREFILE_LIMIT:-0}"
# if they set NICELEVEL in /etc/sysconfig/foo, honor it
[ -n "${NICELEVEL:-}" ] && nice="nice -n $NICELEVEL"
# if they set CGROUP_DAEMON in /etc/sysconfig/foo, honor it
if [ -n "${CGROUP_DAEMON}" ]; then
if [ ! -x /bin/cgexec ]; then
echo -n "Cgroups not installed"; warning
echo
else
cgroup="/bin/cgexec";
for i in $CGROUP_DAEMON; do
cgroup="$cgroup -g $i";
done
fi
fi
# Echo daemon
[ "${BOOTUP:-}" = "verbose" -a -z "${LSB:-}" ] && echo -n " $base"
# And start it up.
if [ -z "$user" ]; then
$cgroup $nice /bin/bash -c "$corelimit >/dev/null 2>&1 ; $*"
else
$cgroup $nice runuser -s /bin/bash $user -c "$corelimit >/dev/null 2>&1 ; $*"
fi
[ "$?" -eq 0 ] && success $"$base startup" || failure $"$base startup"
}
# A function to stop a program.
killproc() {
local RC killlevel= base pid pid_file= delay try
RC=0; delay=3; try=0
# Test syntax.
if [ "$#" -eq 0 ]; then
echo $"Usage: killproc [-p pidfile] [ -d delay] {program} [-signal]"
return 1
fi
if [ "$1" = "-p" ]; then
pid_file=$2
shift 2
fi
if [ "$1" = "-d" ]; then
delay=$(echo $2 | awk -v RS=' ' -v IGNORECASE=1 '{if($1!~/^[0-9.]+[smhd]?$/) exit 1;d=$1~/s$|^[0-9.]*$/?1:$1~/m$/?60:$1~/h$/?60*60:$1~/d$/?24*60*60:-1;if(d==-1) exit 1;delay+=d*$1} END {printf("%d",delay+0.5)}')
if [ "$?" -eq 1 ]; then
echo $"Usage: killproc [-p pidfile] [ -d delay] {program} [-signal]"
return 1
fi
shift 2
fi
# check for second arg to be kill level
[ -n "${2:-}" ] && killlevel=$2
# Save basename.
base=${1##*/}
# Find pid.
__pids_var_run "$1" "$pid_file"
RC=$?
if [ -z "$pid" ]; then
if [ -z "$pid_file" ]; then
pid="$(__pids_pidof "$1")"
else
[ "$RC" = "4" ] && { failure $"$base shutdown" ; return $RC ;}
fi
fi
# Kill it.
if [ -n "$pid" ] ; then
[ "$BOOTUP" = "verbose" -a -z "${LSB:-}" ] && echo -n "$base "
if [ -z "$killlevel" ] ; then
if checkpid $pid 2>&1; then
# TERM first, then KILL if not dead
kill -TERM $pid >/dev/null 2>&1
usleep 50000
if checkpid $pid ; then
try=0
while [ $try -lt $delay ] ; do
checkpid $pid || break
sleep 1
let try+=1
done
if checkpid $pid ; then
kill -KILL $pid >/dev/null 2>&1
usleep 50000
fi
fi
fi
checkpid $pid
RC=$?
[ "$RC" -eq 0 ] && failure $"$base shutdown" || success $"$base shutdown"
RC=$((! $RC))
# use specified level only
else
if checkpid $pid; then
kill $killlevel $pid >/dev/null 2>&1
RC=$?
[ "$RC" -eq 0 ] && success $"$base $killlevel" || failure $"$base $killlevel"
elif [ -n "${LSB:-}" ]; then
RC=7 # Program is not running
fi
fi
else
if [ -n "${LSB:-}" -a -n "$killlevel" ]; then
RC=7 # Program is not running
else
failure $"$base shutdown"
RC=0
fi
fi
# Remove pid file if any.
if [ -z "$killlevel" ]; then
rm -f "${pid_file:-/var/run/$base.pid}"
fi
return $RC
}
# A function to find the pid of a program. Looks *only* at the pidfile
pidfileofproc() {
local pid
# Test syntax.
if [ "$#" = 0 ] ; then
echo $"Usage: pidfileofproc {program}"
return 1
fi
__pids_var_run "$1"
[ -n "$pid" ] && echo $pid
return 0
}
# A function to find the pid of a program.
pidofproc() {
local RC pid pid_file=
# Test syntax.
if [ "$#" = 0 ]; then
echo $"Usage: pidofproc [-p pidfile] {program}"
return 1
fi
if [ "$1" = "-p" ]; then
pid_file=$2
shift 2
fi
fail_code=3 # "Program is not running"
# First try "/var/run/*.pid" files
__pids_var_run "$1" "$pid_file"
RC=$?
if [ -n "$pid" ]; then
echo $pid
return 0
fi
[ -n "$pid_file" ] && return $RC
__pids_pidof "$1" || return $RC
}
status() {
local base pid lock_file= pid_file=
# Test syntax.
if [ "$#" = 0 ] ; then
echo $"Usage: status [-p pidfile] {program}"
return 1
fi
if [ "$1" = "-p" ]; then
pid_file=$2
shift 2
fi
if [ "$1" = "-l" ]; then
lock_file=$2
shift 2
fi
base=${1##*/}
if [ "$_use_systemctl" = "1" ]; then
systemctl status ${0##*/}.service
return $?
fi
# First try "pidof"
__pids_var_run "$1" "$pid_file"
RC=$?
if [ -z "$pid_file" -a -z "$pid" ]; then
pid="$(__pids_pidof "$1")"
fi
if [ -n "$pid" ]; then
echo $"${base} (pid $pid) is running..."
return 0
fi
case "$RC" in
0)
echo $"${base} (pid $pid) is running..."
return 0
;;
1)
echo $"${base} dead but pid file exists"
return 1
;;
4)
echo $"${base} status unknown due to insufficient privileges."
return 4
;;
esac
if [ -z "${lock_file}" ]; then
lock_file=${base}
fi
# See if /var/lock/subsys/${lock_file} exists
if [ -f /var/lock/subsys/${lock_file} ]; then
echo $"${base} dead but subsys locked"
return 2
fi
echo $"${base} is stopped"
return 3
}
echo_success() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_SUCCESS
echo -n $" OK "
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 0
}
echo_failure() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_FAILURE
echo -n $"FAILED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
echo_passed() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"PASSED"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
echo_warning() {
[ "$BOOTUP" = "color" ] && $MOVE_TO_COL
echo -n "["
[ "$BOOTUP" = "color" ] && $SETCOLOR_WARNING
echo -n $"WARNING"
[ "$BOOTUP" = "color" ] && $SETCOLOR_NORMAL
echo -n "]"
echo -ne "\r"
return 1
}
# Inform the graphical boot of our current state
update_boot_stage() {
if [ -x /bin/plymouth ]; then
/bin/plymouth --update="$1"
fi
return 0
}
# Log that something succeeded
success() {
[ "$BOOTUP" != "verbose" -a -z "${LSB:-}" ] && echo_success
return 0
}
# Log that something failed
failure() {
local rc=$?
[ "$BOOTUP" != "verbose" -a -z "${LSB:-}" ] && echo_failure
[ -x /bin/plymouth ] && /bin/plymouth --details
return $rc
}
# Log that something passed, but may have had errors. Useful for fsck
passed() {
local rc=$?
[ "$BOOTUP" != "verbose" -a -z "${LSB:-}" ] && echo_passed
return $rc
}
# Log a warning
warning() {
local rc=$?
[ "$BOOTUP" != "verbose" -a -z "${LSB:-}" ] && echo_warning
return $rc
}
# Run some action. Log its output.
action() {
local STRING rc
STRING=$1
echo -n "$STRING "
shift
"$@" && success $"$STRING" || failure $"$STRING"
rc=$?
echo
return $rc
}
# returns OK if $1 contains $2
strstr() {
[ "${1#*$2*}" = "$1" ] && return 1
return 0
}
# Check whether file $1 is a backup or rpm-generated file and should be ignored
is_ignored_file() {
case "$1" in
*~ | *.bak | *.orig | *.rpmnew | *.rpmorig | *.rpmsave)
return 0
;;
esac
return 1
}
# Evaluate shvar-style booleans
is_true() {
case "$1" in
[tT] | [yY] | [yY][eE][sS] | [tT][rR][uU][eE])
return 0
;;
esac
return 1
}
# Evaluate shvar-style booleans
is_false() {
case "$1" in
[fF] | [nN] | [nN][oO] | [fF][aA][lL][sS][eE])
return 0
;;
esac
return 1
}
# Apply sysctl settings, including files in /etc/sysctl.d
apply_sysctl() {
if [ -x /lib/systemd/systemd-sysctl ]; then
/lib/systemd/systemd-sysctl
else
for file in /usr/lib/sysctl.d/*.conf ; do
is_ignored_file "$file" && continue
[ -f /run/sysctl.d/${file##*/} ] && continue
[ -f /etc/sysctl.d/${file##*/} ] && continue
test -f "$file" && sysctl -e -p "$file" >/dev/null 2>&1
done
for file in /run/sysctl.d/*.conf ; do
is_ignored_file "$file" && continue
[ -f /etc/sysctl.d/${file##*/} ] && continue
test -f "$file" && sysctl -e -p "$file" >/dev/null 2>&1
done
for file in /etc/sysctl.d/*.conf ; do
is_ignored_file "$file" && continue
test -f "$file" && sysctl -e -p "$file" >/dev/null 2>&1
done
sysctl -e -p /etc/sysctl.conf >/dev/null 2>&1
fi
}
# A sed expression to filter out the files that is_ignored_file recognizes
__sed_discard_ignored_files='/\(~\|\.bak\|\.orig\|\.rpmnew\|\.rpmorig\|\.rpmsave\)$/d'
if [ "$_use_systemctl" = "1" ]; then
if [ "x$1" = xstart -o \
"x$1" = xstop -o \
"x$1" = xrestart -o \
"x$1" = xreload -o \
"x$1" = xtry-restart -o \
"x$1" = xforce-reload -o \
"x$1" = xcondrestart ] ; then
systemctl_redirect $0 $1
exit $?
fi
fi

View File

@@ -0,0 +1,9 @@
- model: juser.user
pk: 5000
fields:
username: admin
name: admin
password: pbkdf2_sha256$20000$jBIDGPB2j5JT$orxqGgzzjzykColYm1BswPjgHOiERjZkcgkuVIkD2Hc=
email: admin@jumpserver.org
role: SU
is_active: 1

306
install/install.py Executable file
View File

@@ -0,0 +1,306 @@
#!/usr/bin/python
# coding: utf-8
import time
import os
import sys
from smtplib import SMTP, SMTP_SSL, SMTPAuthenticationError, SMTPConnectError, SMTPSenderRefused
import ConfigParser
import socket
import random
import string
import re
import platform
import shlex
jms_dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
sys.path.append(jms_dir)
def bash(cmd):
"""
run a bash shell command
执行bash命令
"""
return shlex.os.system(cmd)
def valid_ip(ip):
if ('255' in ip) or (ip == "0.0.0.0"):
return False
else:
return True
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',
'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 get_ip_addr():
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
return s.getsockname()[0]
except Exception:
if_data = ''.join(os.popen("LANG=C ifconfig").readlines())
ips = re.findall(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', if_data, flags=re.MULTILINE)
ip = filter(valid_ip, ips)
if ip:
return ip[0]
return ''
class PreSetup(object):
def __init__(self):
self.db_host = '127.0.0.1'
self.db_port = 3306
self.db_user = 'jumpserver'
self.db_pass = '5Lov@wife'
self.db = 'jumpserver'
self.mail_host = 'smtp.qq.com'
self.mail_port = 25
self.mail_addr = 'hello@jumpserver.org'
self.mail_pass = ''
self.ip = ''
self.key = ''.join(random.choice(string.ascii_lowercase + string.digits) \
for _ in range(16))
self.dist = platform.dist()[0].lower()
self.version = platform.dist()[1]
@property
def _is_redhat(self):
if self.dist == "centos" or self.dist == "redhat" or self.dist == "fedora":
return True
@property
def _is_centos7(self):
if self.dist == "centos" and self.version.startswith("7"):
return True
@property
def _is_fedora_new(self):
if self.dist == "fedora" and int(self.version) >= 20:
return True
@property
def _is_ubuntu(self):
if self.dist == "ubuntu" or self.dist == "debian":
return True
def check_platform(self):
if not (self._is_redhat or self._is_ubuntu):
print(u"支持的平台: CentOS, RedHat, Fedora, Debian, Ubuntu, 暂不支持其他平台安装.")
exit()
@staticmethod
def check_bash_return(ret_code, error_msg):
if ret_code != 0:
color_print(error_msg, 'red')
exit()
def write_conf(self, conf_file=os.path.join(jms_dir, 'jumpserver.conf')):
color_print('开始写入配置文件', 'green')
conf = ConfigParser.ConfigParser()
conf.read(conf_file)
conf.set('base', 'url', 'http://%s' % self.ip)
conf.set('base', 'key', self.key)
conf.set('db', 'host', self.db_host)
conf.set('db', 'port', self.db_port)
conf.set('db', 'user', self.db_user)
conf.set('db', 'password', self.db_pass)
conf.set('db', 'database', self.db)
conf.set('mail', 'email_host', self.mail_host)
conf.set('mail', 'email_port', self.mail_port)
conf.set('mail', 'email_host_user', self.mail_addr)
conf.set('mail', 'email_host_password', self.mail_pass)
with open(conf_file, 'w') as f:
conf.write(f)
def _setup_mysql(self):
color_print('开始安装设置mysql (请手动设置mysql安全)', 'green')
color_print('默认用户名: %s 默认密码: %s' % (self.db_user, self.db_pass), 'green')
if self._is_redhat:
if self._is_centos7 or self._is_fedora_new:
ret_code = bash('yum -y install mariadb-server mariadb-devel')
self.check_bash_return(ret_code, "安装mysql(mariadb)失败, 请检查安装源是否更新或手动安装!")
bash('systemctl enable mariadb.service')
bash('systemctl start mariadb.service')
else:
ret_code = bash('yum -y install mysql-server')
self.check_bash_return(ret_code, "安装mysql失败, 请检查安装源是否更新或手动安装!")
bash('service mysqld start')
bash('chkconfig mysqld on')
bash('mysql -e "create database %s default charset=utf8"' % self.db)
bash('mysql -e "grant all on %s.* to \'%s\'@\'%s\' identified by \'%s\'"' % (self.db,
self.db_user,
self.db_host,
self.db_pass))
if self._is_ubuntu:
cmd1 = "echo mysql-server mysql-server/root_password select '' | debconf-set-selections"
cmd2 = "echo mysql-server mysql-server/root_password_again select '' | debconf-set-selections"
cmd3 = "apt-get -y install mysql-server"
ret_code = bash('%s; %s; %s' % (cmd1, cmd2, cmd3))
self.check_bash_return(ret_code, "安装mysql失败, 请检查安装源是否更新或手动安装!")
bash('service mysql start')
bash('mysql -e "create database %s default charset=utf8"' % self.db)
bash('mysql -e "grant all on %s.* to \'%s\'@\'%s\' identified by \'%s\'"' % (self.db,
self.db_user,
self.db_host,
self.db_pass))
def _set_env(self):
color_print('开始关闭防火墙和selinux', 'green')
if self._is_redhat:
os.system("export LANG='en_US.UTF-8'")
if self._is_centos7 or self._is_fedora_new:
cmd1 = "systemctl status firewalld 2> /dev/null 1> /dev/null"
cmd2 = "systemctl stop firewalld"
cmd3 = "systemctl disable firewalld"
bash('%s && %s && %s' % (cmd1, cmd2, cmd3))
bash('localectl set-locale LANG=en_US.UTF-8')
bash('which setenforce 2> /dev/null 1> /dev/null && setenforce 0')
else:
bash("sed -i 's/LANG=.*/LANG=en_US.UTF-8/g' /etc/sysconfig/i18n")
bash('service iptables stop && chkconfig iptables off && setenforce 0')
if self._is_ubuntu:
os.system("export LANG='en_US.UTF-8'")
bash("which iptables && iptables -F")
bash('which setenforce && setenforce 0')
def _test_db_conn(self):
import MySQLdb
try:
MySQLdb.connect(host=self.db_host, port=int(self.db_port),
user=self.db_user, passwd=self.db_pass, db=self.db)
color_print('连接数据库成功', 'green')
return True
except MySQLdb.OperationalError, e:
color_print('数据库连接失败 %s' % e, 'red')
return False
def _test_mail(self):
try:
if self.mail_port == 465:
smtp = SMTP_SSL(self.mail_host, port=self.mail_port, timeout=2)
else:
smtp = SMTP(self.mail_host, port=self.mail_port, timeout=2)
smtp.login(self.mail_addr, self.mail_pass)
smtp.sendmail(self.mail_addr, (self.mail_addr, ),
'''From:%s\r\nTo:%s\r\nSubject:Jumpserver Mail Test!\r\n\r\n Mail test passed!\r\n''' %
(self.mail_addr, self.mail_addr))
smtp.quit()
return True
except Exception, e:
color_print(e, 'red')
skip = raw_input('是否跳过(y/n) [n]? : ')
if skip == 'y':
return True
return False
def _rpm_repo(self):
if self._is_redhat:
color_print('开始安装epel源', 'green')
bash('yum -y install epel-release')
def _depend_rpm(self):
color_print('开始安装依赖包', 'green')
if self._is_redhat:
cmd = 'yum -y install git python-pip mysql-devel rpm-build gcc automake autoconf python-devel vim sshpass lrzsz readline-devel'
ret_code = bash(cmd)
self.check_bash_return(ret_code, "安装依赖失败, 请检查安装源是否更新或手动安装!")
if self._is_ubuntu:
cmd = "apt-get -y --force-yes install git python-pip gcc automake autoconf vim sshpass libmysqld-dev python-all-dev lrzsz libreadline-dev"
ret_code = bash(cmd)
self.check_bash_return(ret_code, "安装依赖失败, 请检查安装源是否更新或手动安装!")
def _require_pip(self):
color_print('开始安装依赖pip包', 'green')
bash('pip uninstall -y pycrypto')
ret_code = bash('pip install -r requirements.txt')
self.check_bash_return(ret_code, "安装JumpServer 依赖的python库失败")
def _input_ip(self):
ip = raw_input('\n请输入您服务器的IP地址用户浏览器可以访问 [%s]: ' % get_ip_addr()).strip()
self.ip = ip if ip else get_ip_addr()
def _input_mysql(self):
while True:
mysql = raw_input('是否安装新的MySQL服务器? (y/n) [y]: ')
if mysql != 'n':
self._setup_mysql()
else:
db_host = raw_input('请输入数据库服务器IP [127.0.0.1]: ').strip()
db_port = raw_input('请输入数据库服务器端口 [3306]: ').strip()
db_user = raw_input('请输入数据库服务器用户 [jumpserver]: ').strip()
db_pass = raw_input('请输入数据库服务器密码: ').strip()
db = raw_input('请输入使用的数据库 [jumpserver]: ').strip()
if db_host: self.db_host = db_host
if db_port: self.db_port = db_port
if db_user: self.db_user = db_user
if db_pass: self.db_pass = db_pass
if db: self.db = db
if self._test_db_conn():
break
print
def _input_smtp(self):
while True:
self.mail_host = raw_input('请输入SMTP地址: ').strip()
mail_port = raw_input('请输入SMTP端口 [25]: ').strip()
self.mail_addr = raw_input('请输入账户: ').strip()
self.mail_pass = raw_input('请输入密码: ').strip()
if mail_port: self.mail_port = int(mail_port)
if self._test_mail():
color_print('\n\t请登陆邮箱查收邮件, 然后确认是否继续安装\n', 'green')
smtp = raw_input('是否继续? (y/n) [y]: ')
if smtp == 'n':
continue
else:
break
print
def start(self):
color_print('请务必先查看wiki https://github.com/jumpserver/jumpserver/wiki')
time.sleep(3)
self.check_platform()
self._rpm_repo()
self._depend_rpm()
self._require_pip()
self._set_env()
self._input_ip()
self._input_mysql()
self._input_smtp()
self.write_conf()
os.system('python %s' % os.path.join(jms_dir, 'install/next.py'))
if __name__ == '__main__':
pre_setup = PreSetup()
pre_setup.start()

113
install/next.py Executable file
View File

@@ -0,0 +1,113 @@
#!/usr/bin/python
# coding: utf-8
import sys
import os
import django
from django.core.management import execute_from_command_line
import shlex
import urllib
import socket
import subprocess
jms_dir = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
sys.path.append(jms_dir)
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
if django.get_version() != '1.6':
setup = django.setup()
from juser.user_api import db_add_user, get_object, User
from install import color_print
from jumpserver.api import get_mac_address, bash
socket.setdefaulttimeout(2)
class Setup(object):
"""
安装jumpserver向导
"""
def __init__(self):
self.admin_user = 'admin'
self.admin_pass = '5Lov@wife'
@staticmethod
def _pull():
color_print('开始更新jumpserver', 'green')
# bash('git pull')
try:
mac = get_mac_address()
version = urllib.urlopen('http://jumpserver.org/version/?id=%s' % mac)
except:
pass
def _input_admin(self):
while True:
print
admin_user = raw_input('请输入管理员用户名 [%s]: ' % self.admin_user).strip()
admin_pass = raw_input('请输入管理员密码: [%s]: ' % self.admin_pass).strip()
admin_pass_again = raw_input('请再次输入管理员密码: [%s]: ' % self.admin_pass).strip()
if admin_user:
self.admin_user = admin_user
if not admin_pass_again:
admin_pass_again = self.admin_pass
if admin_pass:
self.admin_pass = admin_pass
if self.admin_pass != admin_pass_again:
color_print('两次密码不相同请重新输入')
else:
break
print
@staticmethod
def _sync_db():
os.chdir(jms_dir)
execute_from_command_line(['manage.py', 'syncdb', '--noinput'])
def _create_admin(self):
user = get_object(User, username=self.admin_user)
if user:
user.delete()
db_add_user(username=self.admin_user, password=self.admin_pass, role='SU', name='admin', groups='',
admin_groups='', email='admin@jumpserver.org', uuid='MayBeYouAreTheFirstUser', is_active=True)
cmd = 'id %s 2> /dev/null 1> /dev/null || useradd %s' % (self.admin_user, self.admin_user)
shlex.os.system(cmd)
@staticmethod
def _chmod_file():
os.chdir(jms_dir)
os.chmod('init.sh', 0755)
os.chmod('connect.py', 0755)
os.chmod('manage.py', 0755)
os.chmod('run_server.py', 0755)
os.chmod('service.sh', 0755)
os.chmod('logs', 0777)
os.chmod('keys', 0777)
@staticmethod
def _run_service():
cmd = 'bash %s start' % os.path.join(jms_dir, 'service.sh')
shlex.os.system(cmd)
print
color_print('安装成功请访问web, 祝你使用愉快。\n请访问 https://github.com/jumpserver/jumpserver/wiki 查看文档', 'green')
def start(self):
print "开始安装Jumpserver ..."
self._pull()
self._sync_db()
self._input_admin()
self._create_admin()
self._chmod_file()
self._run_service()
if __name__ == '__main__':
setup = Setup()
setup.start()

19
install/requirements.txt Normal file
View File

@@ -0,0 +1,19 @@
#sphinx-me==0.3
django==1.6
pycrypto==2.4.1
paramiko==1.16.0
ecdsa==0.13
MySQL-python==1.2.5
#django-uuidfield==0.5.0
psutil==3.3.0
xlsxwriter==0.7.7
xlrd==0.9.4
django-bootstrap-form==3.2
tornado==4.3
ansible==1.9.4
pyinotify==0.9.6
passlib==1.6.5
argparse==1.4.0
django-crontab==0.6.0
django-smtp-ssl==1.0
pyte==0.5.2

403
jasset/asset_api.py Normal file
View File

@@ -0,0 +1,403 @@
# coding: utf-8
from __future__ import division
import xlrd
import xlsxwriter
from django.db.models import AutoField
from jumpserver.api import *
from jasset.models import ASSET_STATUS, ASSET_TYPE, ASSET_ENV, IDC, AssetRecord
from jperm.ansible_api import MyRunner
from jperm.perm_api import gen_resource
from jumpserver.templatetags.mytags import get_disk_info
import traceback
def group_add_asset(group, asset_id=None, asset_ip=None):
"""
资产组添加资产
Asset group add a asset
"""
if asset_id:
asset = get_object(Asset, id=asset_id)
else:
asset = get_object(Asset, ip=asset_ip)
if asset:
group.asset_set.add(asset)
def db_add_group(**kwargs):
"""
add a asset group in database
数据库中添加资产
"""
name = kwargs.get('name')
group = get_object(AssetGroup, name=name)
asset_id_list = kwargs.pop('asset_select')
if not group:
group = AssetGroup(**kwargs)
group.save()
for asset_id in asset_id_list:
group_add_asset(group, asset_id)
def db_update_group(**kwargs):
"""
add a asset group in database
数据库中更新资产
"""
group_id = kwargs.pop('id')
asset_id_list = kwargs.pop('asset_select')
group = get_object(AssetGroup, id=group_id)
for asset_id in asset_id_list:
group_add_asset(group, asset_id)
AssetGroup.objects.filter(id=group_id).update(**kwargs)
def db_asset_add(**kwargs):
"""
add asset to db
添加主机时数据库操作函数
"""
group_id_list = kwargs.pop('groups')
asset = Asset(**kwargs)
asset.save()
group_select = []
for group_id in group_id_list:
group = AssetGroup.objects.filter(id=group_id)
group_select.extend(group)
asset.group = group_select
def db_asset_update(**kwargs):
""" 修改主机时数据库操作函数 """
asset_id = kwargs.pop('id')
Asset.objects.filter(id=asset_id).update(**kwargs)
def sort_ip_list(ip_list):
""" ip地址排序 """
ip_list.sort(key=lambda s: map(int, s.split('.')))
return ip_list
def get_tuple_name(asset_tuple, value):
""""""
for t in asset_tuple:
if t[0] == value:
return t[1]
return ''
def get_tuple_diff(asset_tuple, field_name, value):
""""""
old_name = get_tuple_name(asset_tuple, int(value[0])) if value[0] else u''
new_name = get_tuple_name(asset_tuple, int(value[1])) if value[1] else u''
alert_info = [field_name, old_name, new_name]
return alert_info
def asset_diff(before, after):
"""
asset change before and after
"""
alter_dic = {}
before_dic, after_dic = before, dict(after.iterlists())
for k, v in before_dic.items():
after_dic_values = after_dic.get(k, [])
if k == 'group':
after_dic_value = after_dic_values if len(after_dic_values) > 0 else u''
uv = v if v is not None else u''
else:
after_dic_value = after_dic_values[0] if len(after_dic_values) > 0 else u''
uv = unicode(v) if v is not None else u''
if uv != after_dic_value:
alter_dic.update({k: [uv, after_dic_value]})
for k, v in alter_dic.items():
if v == [None, u'']:
alter_dic.pop(k)
return alter_dic
def asset_diff_one(before, after):
print before.__dict__, after.__dict__
fields = Asset._meta.get_all_field_names()
for field in fields:
print before.field, after.field
def db_asset_alert(asset, username, alert_dic):
"""
asset alert info to db
"""
alert_list = []
asset_tuple_dic = {'status': ASSET_STATUS, 'env': ASSET_ENV, 'asset_type': ASSET_TYPE}
for field, value in alert_dic.iteritems():
field_name = Asset._meta.get_field_by_name(field)[0].verbose_name
if field == 'idc':
old = IDC.objects.filter(id=value[0]) if value[0] else u''
new = IDC.objects.filter(id=value[1]) if value[1] else u''
old_name = old[0].name if old else u''
new_name = new[0].name if new else u''
alert_info = [field_name, old_name, new_name]
elif field in ['status', 'env', 'asset_type']:
alert_info = get_tuple_diff(asset_tuple_dic.get(field), field_name, value)
elif field == 'group':
old, new = [], []
for group_id in value[0]:
group_name = AssetGroup.objects.get(id=int(group_id)).name
old.append(group_name)
for group_id in value[1]:
group_name = AssetGroup.objects.get(id=int(group_id)).name
new.append(group_name)
if sorted(old) == sorted(new):
continue
else:
alert_info = [field_name, ','.join(old), ','.join(new)]
elif field == 'use_default_auth':
if unicode(value[0]) == 'True' and unicode(value[1]) == 'on' or \
unicode(value[0]) == 'False' and unicode(value[1]) == '':
continue
else:
name = asset.username
alert_info = [field_name, u'默认', name] if unicode(value[0]) == 'True' else \
[field_name, name, u'默认']
elif field in ['username', 'password']:
continue
elif field == 'is_active':
if unicode(value[0]) == 'True' and unicode(value[1]) == '1' or \
unicode(value[0]) == 'False' and unicode(value[1]) == '0':
continue
else:
alert_info = [u'是否激活', u'激活', u'禁用'] if unicode(value[0]) == 'True' else \
[u'是否激活', u'禁用', u'激活']
else:
alert_info = [field_name, unicode(value[0]), unicode(value[1])]
if 'alert_info' in dir():
alert_list.append(alert_info)
if alert_list:
AssetRecord.objects.create(asset=asset, username=username, content=alert_list)
def write_excel(asset_all):
data = []
now = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M')
file_name = 'cmdb_excel_' + now + '.xlsx'
workbook = xlsxwriter.Workbook('static/files/excels/%s' % file_name)
worksheet = workbook.add_worksheet(u'CMDB数据')
worksheet.set_first_sheet()
worksheet.set_column('A:E', 15)
worksheet.set_column('F:F', 40)
worksheet.set_column('G:Z', 15)
title = [u'主机名', u'IP', u'IDC', u'所属主机组', u'操作系统', u'CPU', u'内存(G)', u'硬盘(G)',
u'机柜位置', u'MAC', u'远控IP', u'机器状态', u'备注']
for asset in asset_all:
group_list = []
for p in asset.group.all():
group_list.append(p.name)
disk = get_disk_info(asset.disk)
group_all = '/'.join(group_list)
status = asset.get_status_display()
idc_name = asset.idc.name if asset.idc else u''
system_type = asset.system_type if asset.system_type else u''
system_version = asset.system_version if asset.system_version else u''
system_os = unicode(system_type) + unicode(system_version)
alter_dic = [asset.hostname, asset.ip, idc_name, group_all, system_os, asset.cpu, asset.memory,
disk, asset.cabinet, asset.mac, asset.remote_ip, status, asset.comment]
data.append(alter_dic)
format = workbook.add_format()
format.set_border(1)
format.set_align('center')
format.set_align('vcenter')
format.set_text_wrap()
format_title = workbook.add_format()
format_title.set_border(1)
format_title.set_bg_color('#cccccc')
format_title.set_align('center')
format_title.set_bold()
format_ave = workbook.add_format()
format_ave.set_border(1)
format_ave.set_num_format('0.00')
worksheet.write_row('A1', title, format_title)
i = 2
for alter_dic in data:
location = 'A' + str(i)
worksheet.write_row(location, alter_dic, format)
i += 1
workbook.close()
ret = (True, file_name)
return ret
def copy_model_instance(obj):
initial = dict([(f.name, getattr(obj, f.name))
for f in obj._meta.fields
if not isinstance(f, AutoField) and \
not f in obj._meta.parents.values()])
return obj.__class__(**initial)
def ansible_record(asset, ansible_dic, username):
alert_dic = {}
asset_dic = asset.__dict__
for field, value in ansible_dic.items():
old = asset_dic.get(field)
new = ansible_dic.get(field)
if unicode(old) != unicode(new):
setattr(asset, field, value)
asset.save()
alert_dic[field] = [old, new]
db_asset_alert(asset, username, alert_dic)
def excel_to_db(excel_file):
"""
Asset add batch function
"""
try:
data = xlrd.open_workbook(filename=None, file_contents=excel_file.read())
except Exception, e:
return False
else:
table = data.sheets()[0]
rows = table.nrows
for row_num in range(1, rows):
row = table.row_values(row_num)
if row:
group_instance = []
ip, port, hostname, use_default_auth, username, password, group = row
if get_object(Asset, hostname=hostname):
continue
if isinstance(password, int) or isinstance(password, float):
password = unicode(int(password))
use_default_auth = 1 if use_default_auth == u'默认' else 0
password_encode = CRYPTOR.encrypt(password) if password else ''
if hostname:
asset = Asset(ip=ip,
port=port,
hostname=hostname,
use_default_auth=use_default_auth,
username=username,
password=password_encode
)
asset.save()
group_list = group.split('/')
for group_name in group_list:
group = get_object(AssetGroup, name=group_name)
if group:
group_instance.append(group)
if group_instance:
asset.group = group_instance
asset.save()
return True
def get_ansible_asset_info(asset_ip, setup_info):
disk_need = {}
disk_all = setup_info.get("ansible_devices")
if disk_all:
for disk_name, disk_info in disk_all.iteritems():
if disk_name.startswith('sd') or disk_name.startswith('hd') or disk_name.startswith('vd') or disk_name.startswith('xvd'):
disk_size = disk_info.get("size", '')
if 'M' in disk_size:
disk_format = round(float(disk_size[:-2]) / 1000, 0)
elif 'T' in disk_size:
disk_format = round(float(disk_size[:-2]) * 1000, 0)
else:
disk_format = float(disk_size[:-2])
disk_need[disk_name] = disk_format
all_ip = setup_info.get("ansible_all_ipv4_addresses")
other_ip_list = all_ip.remove(asset_ip) if asset_ip in all_ip else []
other_ip = ','.join(other_ip_list) if other_ip_list else ''
# hostname = setup_info.get("ansible_hostname")
# ip = setup_info.get("ansible_default_ipv4").get("address")
mac = setup_info.get("ansible_default_ipv4").get("macaddress")
brand = setup_info.get("ansible_product_name")
try:
cpu_type = setup_info.get("ansible_processor")[1]
except IndexError:
cpu_type = ' '.join(setup_info.get("ansible_processor")[0].split(' ')[:6])
memory = setup_info.get("ansible_memtotal_mb")
try:
memory_format = int(round((int(memory) / 1000), 0))
except Exception:
memory_format = memory
disk = disk_need
system_type = setup_info.get("ansible_distribution")
if system_type.lower() == "freebsd":
system_version = setup_info.get("ansible_distribution_release")
cpu_cores = setup_info.get("ansible_processor_count")
else:
system_version = setup_info.get("ansible_distribution_version")
cpu_cores = setup_info.get("ansible_processor_vcpus")
cpu = cpu_type + ' * ' + unicode(cpu_cores)
system_arch = setup_info.get("ansible_architecture")
# asset_type = setup_info.get("ansible_system")
sn = setup_info.get("ansible_product_serial")
asset_info = [other_ip, mac, cpu, memory_format, disk, sn, system_type, system_version, brand, system_arch]
return asset_info
def asset_ansible_update(obj_list, name=''):
resource = gen_resource(obj_list)
ansible_instance = MyRunner(resource)
ansible_asset_info = ansible_instance.run(module_name='setup', pattern='*')
logger.debug('获取硬件信息: %s' % ansible_asset_info)
for asset in obj_list:
try:
setup_info = ansible_asset_info['contacted'][asset.hostname]['ansible_facts']
logger.debug("setup_info: %s" % setup_info)
except KeyError, e:
logger.error("获取setup_info失败: %s" % e)
continue
else:
try:
asset_info = get_ansible_asset_info(asset.ip, setup_info)
print asset_info
other_ip, mac, cpu, memory, disk, sn, system_type, system_version, brand, system_arch = asset_info
asset_dic = {"other_ip": other_ip,
"mac": mac,
"cpu": cpu,
"memory": memory,
"disk": disk,
"sn": sn,
"system_type": system_type,
"system_version": system_version,
"system_arch": system_arch,
"brand": brand
}
ansible_record(asset, asset_dic, name)
except Exception as e:
logger.error("save setup info failed! %s" % e)
traceback.print_exc()
def asset_ansible_update_all():
name = u'定时更新'
asset_all = Asset.objects.all()
asset_ansible_update(asset_all, name)

38
jasset/forms.py Normal file
View File

@@ -0,0 +1,38 @@
# coding:utf-8
from django import forms
from jasset.models import IDC, Asset, AssetGroup
class AssetForm(forms.ModelForm):
class Meta:
model = Asset
fields = [
"ip", "other_ip", "hostname", "port", "group", "username", "password", "use_default_auth",
"idc", "mac", "remote_ip", "brand", "cpu", "memory", "disk", "system_type", "system_version",
"cabinet", "position", "number", "status", "asset_type", "env", "sn", "is_active", "comment",
"system_arch"
]
class AssetGroupForm(forms.ModelForm):
class Meta:
model = AssetGroup
fields = [
"name", "comment"
]
class IdcForm(forms.ModelForm):
class Meta:
model = IDC
fields = ['name', "bandwidth", "operator", 'linkman', 'phone', 'address', 'network', 'comment']
widgets = {
'name': forms.TextInput(attrs={'placeholder': 'Name'}),
'network': forms.Textarea(
attrs={'placeholder': '192.168.1.0/24\n192.168.2.0/24'})
}

111
jasset/models.py Normal file
View File

@@ -0,0 +1,111 @@
# coding: utf-8
import datetime
from django.db import models
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 AssetGroup(models.Model):
GROUP_TYPE = (
('P', 'PRIVATE'),
('A', 'ASSET'),
)
name = models.CharField(max_length=80, unique=True)
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):
"""
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=1024, 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)
asset = models.ForeignKey(Asset)
alias = models.CharField(max_length=100, blank=True, null=True)
def __unicode__(self):
return self.alias

24
jasset/urls.py Normal file
View File

@@ -0,0 +1,24 @@
# coding:utf-8
from django.conf.urls import patterns, include, url
from jasset.views import *
urlpatterns = patterns('',
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'),
)

570
jasset/views.py Normal file
View File

@@ -0,0 +1,570 @@
# coding:utf-8
from django.db.models import Q
from jasset.asset_api import *
from jumpserver.api import *
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
@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':
name = request.POST.get('name', '')
asset_select = request.POST.getlist('asset_select', [])
comment = request.POST.get('comment', '')
try:
if not name:
emg = u'组名不能为空'
raise ServerError(emg)
asset_group_test = get_object(AssetGroup, name=name)
if asset_group_test:
emg = u"该组名 %s 已存在" % name
raise ServerError(emg)
except ServerError:
pass
else:
db_add_group(name=name, comment=comment, asset_select=asset_select)
smg = u"主机组 %s 添加成功" % name
return my_render('jasset/group_add.html', locals(), 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':
name = request.POST.get('name', '')
asset_select = request.POST.getlist('asset_select', [])
comment = request.POST.get('comment', '')
try:
if not name:
emg = u'组名不能为空'
raise ServerError(emg)
if group.name != name:
asset_group_test = get_object(AssetGroup, name=name)
if asset_group_test:
emg = u"该组名 %s 已存在" % name
raise ServerError(emg)
except ServerError:
pass
else:
group.asset_set.clear()
db_update_group(id=group_id, name=name, comment=comment, asset_select=asset_select)
smg = u"主机组 %s 添加成功" % name
return HttpResponseRedirect(reverse('asset_group_list'))
return my_render('jasset/group_edit.html', locals(), request)
@require_role('admin')
def group_list(request):
"""
list asset group
列出资产组
"""
header_title, path1, path2 = u'查看资产组', u'资产管理', u'查看资产组'
keyword = request.GET.get('keyword', '')
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))
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)
@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=unicode(hostname)):
error = u'该主机名 %s 已存在!' % hostname
raise ServerError(error)
except ServerError:
pass
else:
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()
msg = u'主机 %s 添加成功' % hostname
else:
esg = u'主机 %s 添加失败' % hostname
return my_render('jasset/asset_add.html', locals(), request)
@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_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':
asset_batch = request.GET.get('arg', '')
asset_id_all = str(request.POST.get('asset_id_all', ''))
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:
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 修改成功' % 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/asset_edit.html', locals(), request)
@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:
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'])
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':
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])
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 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)
return my_render('jasset/asset_edit_batch.html', locals(), request)
@require_role('admin')
def asset_detail(request):
"""
Asset detail view
"""
header_title, path1, path2 = u'主机详细信息', u'资产管理', u'主机详情'
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
asset_record = AssetRecord.objects.filter(asset=asset).order_by('-alert_time')
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:
asset_ansible_update([asset], name)
return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id)
@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 add view
"""
header_title, path1, path2 = u'添加IDC', u'资产管理', u'添加IDC'
if request.method == 'POST':
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_role('admin')
def idc_list(request):
"""
IDC list view
"""
header_title, path1, path2 = u'查看IDC', u'资产管理', u'查看IDC'
posts = IDC.objects.all()
keyword = request.GET.get('keyword', '')
if keyword:
posts = IDC.objects.filter(Q(name__contains=keyword) | Q(comment__contains=keyword))
else:
posts = IDC.objects.exclude(name='ALL').order_by('id')
contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request)
return my_render('jasset/idc_list.html', locals(), request)
@require_role('admin')
def idc_edit(request):
"""
IDC edit view
"""
header_title, path1, path2 = u'编辑IDC', u'资产管理', u'编辑IDC'
idc_id = request.GET.get('id', '')
idc = get_object(IDC, id=idc_id)
if request.method == 'POST':
idc_form = IdcForm(request.POST, instance=idc)
if idc_form.is_valid():
idc_form.save()
return HttpResponseRedirect(reverse('idc_list'))
else:
idc_form = IdcForm(instance=idc)
return my_render('jasset/idc_edit.html', locals(), request)
@require_role('admin')
def idc_del(request):
"""
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_role('admin')
def asset_upload(request):
"""
Upload asset excel file view
"""
if request.method == 'POST':
excel_file = request.FILES.get('file_name', '')
ret = excel_to_db(excel_file)
if ret:
smg = u'批量添加成功'
else:
emg = u'批量添加失败,请检查格式.'
return my_render('jasset/asset_add_batch.html', locals(), request)

121
jlog/log_api.py Normal file
View File

@@ -0,0 +1,121 @@
# coding: utf-8
from argparse import ArgumentParser, FileType
from contextlib import closing
from io import open as copen
from json import dumps
from math import ceil
import datetime
import time
import re
import os
from os.path import basename, dirname, exists, join
from struct import unpack
from subprocess import Popen
from sys import platform, prefix, stderr
from tempfile import NamedTemporaryFile
from jinja2 import FileSystemLoader, Template
from jinja2.environment import Environment
from jumpserver.api import BASE_DIR, logger
from jlog.models import Log
DEFAULT_TEMPLATE = join(BASE_DIR, 'templates', 'jlog', 'static.jinja2')
rz_pat = re.compile(r'\x18B\w+\r\x8a(\x11)?')
def escapeString(string):
string = rz_pat.sub('', string)
try:
string = string.encode('unicode_escape').decode('utf-8', 'ignore')
except (UnicodeEncodeError, UnicodeDecodeError):
string = string.decode('utf-8', 'ignore')
string = string.replace("'", "\\'")
string = '\'' + string + '\''
return string
def getTiming(timef):
timing = None
with closing(timef):
timing = [l.strip().split(' ') for l in timef]
timing = [(int(ceil(float(r[0]) * 1000)), int(r[1])) for r in timing]
return timing
def scriptToJSON(scriptf, timing=None):
ret = []
with closing(scriptf):
scriptf.readline() # ignore first header line from script file
offset = 0
for t in timing:
dt = scriptf.read(t[1])
data = escapeString(dt)
# print ('###### (%s, %s)' % (t[1], repr(data)))
offset += t[0]
ret.append((data, offset))
return dumps(ret)
def renderTemplate(script_path, time_file_path, dimensions=(24, 80), templatename=DEFAULT_TEMPLATE):
with copen(script_path, encoding='utf-8', errors='replace', newline='\r\n') as scriptf:
# with open(script_path) as scriptf:
with open(time_file_path) as timef:
timing = getTiming(timef)
json = scriptToJSON(scriptf, timing)
fsl = FileSystemLoader(dirname(templatename), 'utf-8')
e = Environment()
e.loader = fsl
templatename = basename(templatename)
rendered = e.get_template(templatename).render(json=json,
dimensions=dimensions)
return rendered
def renderJSON(script_path, time_file_path):
with copen(script_path, encoding='utf-8', errors='replace', newline='\r\n') as scriptf:
# with open(script_path) as scriptf:
with open(time_file_path) as timef:
timing = getTiming(timef)
ret = {}
with closing(scriptf):
scriptf.readline() # ignore first header line from script file
offset = 0
for t in timing:
dt = scriptf.read(t[1])
offset += t[0]
ret[str(offset/float(1000))] = dt.decode('utf-8', 'replace')
return dumps(ret)
def kill_invalid_connection():
unfinished_logs = Log.objects.filter(is_finished=False)
now = datetime.datetime.now()
now_timestamp = int(time.mktime(now.timetuple()))
for log in unfinished_logs:
try:
log_file_mtime = int(os.stat('%s.log' % log.log_path).st_mtime)
except OSError:
log_file_mtime = 0
if (now_timestamp - log_file_mtime) > 3600:
if log.login_type == 'ssh':
try:
os.kill(int(log.pid), 9)
except OSError:
pass
elif (now - log.start_time).days < 1:
continue
log.is_finished = True
log.end_time = now
log.save()
logger.warn('kill log %s' % log.log_path)

75
jlog/models.py Normal file
View File

@@ -0,0 +1,75 @@
from django.db import models
from juser.models import User
import time
class Log(models.Model):
user = models.CharField(max_length=20, null=True)
host = models.CharField(max_length=200, null=True)
remote_ip = models.CharField(max_length=100)
login_type = models.CharField(max_length=100)
log_path = models.CharField(max_length=100)
start_time = models.DateTimeField(null=True)
pid = models.IntegerField()
is_finished = models.BooleanField(default=False)
end_time = models.DateTimeField(null=True)
filename = models.CharField(max_length=40)
'''
add by liuzheng
'''
# userMM = models.ManyToManyField(User)
# logPath = models.TextField()
# filename = models.CharField(max_length=40)
# logPWD = models.TextField() # log zip file's
# nick = models.TextField(null=True) # log's nick name
# log = models.TextField(null=True)
# history = models.TextField(null=True)
# timestamp = models.IntegerField(default=int(time.time()))
# datetimestamp = models.DateTimeField(auto_now_add=True)
def __unicode__(self):
return self.log_path
class Alert(models.Model):
msg = models.CharField(max_length=20)
time = models.DateTimeField(null=True)
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)
class TermLog(models.Model):
user = models.ManyToManyField(User)
logPath = models.TextField()
filename = models.CharField(max_length=40)
logPWD = models.TextField() # log zip file's
nick = models.TextField(null=True) # log's nick name
log = models.TextField(null=True)
history = models.TextField(null=True)
timestamp = models.IntegerField(default=int(time.time()))
datetimestamp = models.DateTimeField(auto_now_add=True)

11
jlog/urls.py Normal file
View File

@@ -0,0 +1,11 @@
# coding:utf-8
from django.conf.urls import patterns, include, url
from jlog.views import *
urlpatterns = patterns('',
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'),
)

377
jlog/views.py Normal file
View File

@@ -0,0 +1,377 @@
# coding:utf-8
from django.db.models import Q
from django.template import RequestContext
from django.shortcuts import render_to_response, render
from jumpserver.api import *
from jperm.perm_api import user_have_perm
from django.http import HttpResponseNotFound
from jlog.log_api import renderJSON
from jlog.models import Log, ExecLog, FileLog, TermLog
from jumpserver.settings import LOG_DIR
import zipfile
import json
import pyte
@require_role('admin')
def log_list(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)
session_id = request.session.session_key
return render_to_response('jlog/log_%s.html' % offset, locals(), context_instance=RequestContext(request))
@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]
try:
os.kill(int(pid), 9)
except OSError:
pass
Log.objects.filter(pid=pid).update(is_finished=1, end_time=datetime.datetime.now())
return render_to_response('jlog/log_offline.html', locals(), context_instance=RequestContext(request))
else:
return HttpResponseNotFound(u'没有此进程!')
@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]
# 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 HttpResponse('无日志记录!')
@require_role('admin')
def log_record(request):
"""
Author: liuzheng712@gmail.com
"""
if request.method == "GET":
return render(request, 'jlog/record.html')
elif request.method == "POST":
log_id = request.REQUEST.get('id', None)
if log_id:
TermL = TermLogRecorder(request.user)
log = Log.objects.get(id=int(log_id))
if len(log.filename) == 0:
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 = renderJSON(log_file, log_time)
return HttpResponse(content)
else:
return HttpResponse(TermL.load_full_log(filename=log.filename))
else:
return HttpResponse("ERROR")
else:
return HttpResponse("ERROR METHOD!")
@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)
class TermLogRecorder(object):
"""
TermLogRecorder
---
Author: liuzheng <liuzheng712@gmail>
This class is use for record the terminal output log.
self.commands is pure commands list, it will have empty item '' because in vi/vim model , I made it log noting.
self.CMD is the command with timestamp, like this {'1458723794.88': u'ls', '1458723799.82': u'tree'}.
self.log is the all output with delta time log.
self.vim_pattern is the regexp for check vi/vim/fg model.
Usage:
recorder = TermLogRecorder(user=UserObject) # or recorder = TermLogRecorder(uid=UserID)
recoder.write(messages)
recoder.save() # save all log into database
# The following methods all have `user`,`uid`,args. Same as __init__
list = recoder.list() # will give a object about this user's all log info
recoder.load_full_log(filemane) # will get full log
recoder.load_history(filename) # will only get the command history list
recoder.share_to(filename,user=UserObject) # or recoder.share_to(filename,uid=UserID). will share this commands to someone
recoder.unshare_to(filename,user=UserObject) # or recoder.unshare_to(filename,uid=UserID). will unshare this commands to someone
recoder.setid(id) # registered this term with an id, for monitor
"""
loglist = dict()
def __init__(self, user=None, uid=None):
self.log = {}
self.id = 0
if isinstance(user, User):
self.user = user
elif uid:
self.user = User.objects.get(id=uid)
else:
self.user = None
self.recoderStartTime = time.time()
self.__init_screen_stream()
self.recoder = False
self.commands = []
self._lists = None
self.file = None
self.filename = None
self._data = None
self.vim_pattern = re.compile(r'\W?vi[m]?\s.* | \W?fg\s.*', re.X)
self._in_vim = False
self.CMD = {}
def __init_screen_stream(self):
"""
Initializing the virtual screen and the character stream
"""
self._stream = pyte.ByteStream()
self._screen = pyte.Screen(80, 24)
self._stream.attach(self._screen)
def _command(self):
for i in self._screen.display:
if i.strip().__len__() > 0:
self.commands.append(i.strip())
if not i.strip() == '':
self.CMD[str(time.time())] = self.commands[-1]
self._screen.reset()
def setid(self, id):
self.id = id
TermLogRecorder.loglist[str(id)] = [self]
def write(self, msg):
if self.recoder and (not self._in_vim):
if self.commands.__len__() == 0:
self._stream.feed(msg)
elif not self.vim_pattern.search(self.commands[-1]):
self._stream.feed(msg)
else:
self._in_vim = True
self._command()
else:
if self._in_vim:
if re.compile(r'\[\?1049', re.X).search(msg.decode('utf-8', 'replace')):
self._in_vim = False
self.commands.append('')
self._screen.reset()
else:
self._command()
try:
self.write_message(msg)
except:
pass
# print "<<<<<<<<<<<<<<<<"
# print self.commands
# print self.CMD
# print ">>>>>>>>>>>>>>>>"
self.log[str(time.time() - self.recoderStartTime)] = msg.decode('utf-8', 'replace')
def save(self, path=LOG_DIR):
date = datetime.datetime.now().strftime('%Y%m%d')
filename = str(uuid.uuid4())
self.filename = filename
filepath = os.path.join(path, 'tty', date, filename + '.zip')
if not os.path.isdir(os.path.join(path, 'tty', date)):
os.makedirs(os.path.join(path, 'tty', date), mode=0777)
while os.path.isfile(filepath):
filename = str(uuid.uuid4())
filepath = os.path.join(path, 'tty', date, filename + '.zip')
password = str(uuid.uuid4())
try:
zf = zipfile.ZipFile(filepath, 'w', zipfile.ZIP_DEFLATED)
zf.setpassword(password)
zf.writestr(filename, json.dumps(self.log))
zf.close()
record = TermLog.objects.create(logPath=filepath, logPWD=password, filename=filename,
history=json.dumps(self.CMD), timestamp=int(self.recoderStartTime))
if self.user:
record.user.add(self.user)
except:
record = TermLog.objects.create(logPath='locale', logPWD=password, log=json.dumps(self.log),
filename=filename, history=json.dumps(self.CMD),
timestamp=int(self.recoderStartTime))
if self.user:
record.user.add(self.user)
try:
del TermLogRecorder.loglist[str(self.id)]
except KeyError:
pass
def list(self, user=None, uid=None):
tmp = []
if isinstance(user, User):
user = user
elif uid:
user = User.objects.get(id=uid)
else:
user = self.user
if user:
self._lists = TermLog.objects.filter(user=user.id)
for i in self._lists.all():
tmp.append(
{'filename': i.filename, 'locale': i.logPath == 'locale', 'nick': i.nick, 'timestamp': i.timestamp,
'date': i.datetimestamp})
return tmp
def load_full_log(self, filename, user=None, uid=None):
if isinstance(user, User):
user = user
elif uid:
user = User.objects.get(id=uid)
else:
user = self.user
if user:
if self._lists:
self.file = self._lists.get(filename=filename)
else:
self.file = TermLog.objects.get(filename=filename)
if self.file.logPath == 'locale':
return self.file.log
else:
try:
zf = zipfile.ZipFile(self.file.logPath, 'r', zipfile.ZIP_DEFLATED)
zf.setpassword(self.file.logPWD)
self._data = zf.read(zf.namelist()[0])
return self._data
except KeyError:
return 'ERROR: Did not find %s file' % filename
return 'ERROR User(None)'
def load_history(self, filename, user=None, uid=None):
if isinstance(user, User):
user = user
elif uid:
user = User.objects.get(id=uid)
else:
user = self.user
if user:
if self._lists:
self.file = self._lists.get(filename=filename)
else:
self.file = TermLog.objects.get(filename=filename)
return self.file.history
return 'ERROR User(None)'
def share_to(self, filename, user=None, uid=None):
if isinstance(user, User):
user = user
elif uid:
user = User.objects.get(id=uid)
else:
pass
if user:
TermLog.objects.get(filename=filename).user.add(user)
return True
return False
def unshare_to(self, filename, user=None, uid=None):
if isinstance(user, User):
user = user
elif uid:
user = User.objects.get(id=uid)
else:
pass
if user:
TermLog.objects.get(filename=filename).user.remove(user)
return True
return False

12
jperm/README.md Normal file
View File

@@ -0,0 +1,12 @@
# Jperm App
---
### 模块 ansible_api
> 使用说明
+ 依赖rpm安装包 ansible、 sshpass
+ 依赖pip安装包 passlib
+ 关于ansible配置 需要启用配置文件(/etc/ansible/ansible.cfg)的 host_key_checking = False

3
jperm/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

517
jperm/ansible_api.py Normal file
View File

@@ -0,0 +1,517 @@
# -*- coding: utf-8 -*-
from tempfile import NamedTemporaryFile
import os.path
from ansible.inventory.group import Group
from ansible.inventory.host import Host
from ansible.inventory import Inventory
from ansible.runner import Runner
from ansible.playbook import PlayBook
from ansible import callbacks
from ansible import utils
import ansible.constants as C
from passlib.hash import sha512_crypt
from django.template.loader import get_template
from django.template import Context
from jumpserver.api import logger
API_DIR = os.path.dirname(os.path.abspath(__file__))
ANSIBLE_DIR = os.path.join(API_DIR, 'playbooks')
C.HOST_KEY_CHECKING = False
class AnsibleError(StandardError):
"""
the base AnsibleError which contains error(required),
data(optional) and message(optional).
存储所有Ansible 异常对象
"""
def __init__(self, error, data='', message=''):
super(AnsibleError, self).__init__(message)
self.error = error
self.data = data
self.message = message
class CommandValueError(AnsibleError):
"""
indicate the input value has error or invalid.
the data specifies the error field of input form.
输入不合法 异常对象
"""
def __init__(self, field, message=''):
super(CommandValueError, self).__init__('value:invalid', field, message)
class MyInventory(Inventory):
"""
this is my ansible inventory object.
"""
def __init__(self, resource):
"""
resource的数据格式是一个列表字典比如
{
"group1": {
"hosts": [{"hostname": "10.10.10.10", "port": "22", "username": "test", "password": "mypass"}, ...],
"vars": {"var1": value1, "var2": value2, ...}
}
}
如果你只传入1个列表这默认该列表内的所有主机属于my_group组,比如
[{"hostname": "10.10.10.10", "port": "22", "username": "test", "password": "mypass"}, ...]
"""
self.resource = resource
self.inventory = Inventory(host_list=[])
self.gen_inventory()
def my_add_group(self, hosts, groupname, groupvars=None):
"""
add hosts to a group
"""
my_group = Group(name=groupname)
# if group variables exists, add them to group
if groupvars:
for key, value in groupvars.iteritems():
my_group.set_variable(key, value)
# add hosts to group
for host in hosts:
# set connection variables
hostname = host.get("hostname")
hostip = host.get('ip', hostname)
hostport = host.get("port")
username = host.get("username")
password = host.get("password")
ssh_key = host.get("ssh_key")
my_host = Host(name=hostname, port=hostport)
my_host.set_variable('ansible_ssh_host', hostip)
my_host.set_variable('ansible_ssh_port', hostport)
my_host.set_variable('ansible_ssh_user', username)
my_host.set_variable('ansible_ssh_pass', password)
my_host.set_variable('ansible_ssh_private_key_file', ssh_key)
# set other variables
for key, value in host.iteritems():
if key not in ["hostname", "port", "username", "password"]:
my_host.set_variable(key, value)
# add to group
my_group.add_host(my_host)
self.inventory.add_group(my_group)
def gen_inventory(self):
"""
add hosts to inventory.
"""
if isinstance(self.resource, list):
self.my_add_group(self.resource, 'default_group')
elif isinstance(self.resource, dict):
for groupname, hosts_and_vars in self.resource.iteritems():
self.my_add_group(hosts_and_vars.get("hosts"), groupname, hosts_and_vars.get("vars"))
class MyRunner(MyInventory):
"""
This is a General object for parallel execute modules.
"""
def __init__(self, *args, **kwargs):
super(MyRunner, self).__init__(*args, **kwargs)
self.results_raw = {}
def run(self, module_name='shell', module_args='', timeout=10, forks=10, pattern='*',
become=False, become_method='sudo', become_user='root', become_pass='', transport='paramiko'):
"""
run module from andible ad-hoc.
module_name: ansible module_name
module_args: ansible module args
"""
hoc = Runner(module_name=module_name,
module_args=module_args,
timeout=timeout,
inventory=self.inventory,
pattern=pattern,
forks=forks,
become=become,
become_method=become_method,
become_user=become_user,
become_pass=become_pass,
transport=transport
)
self.results_raw = hoc.run()
logger.debug(self.results_raw)
return self.results_raw
@property
def results(self):
"""
{'failed': {'localhost': ''}, 'ok': {'jumpserver': ''}}
"""
result = {'failed': {}, 'ok': {}}
dark = self.results_raw.get('dark')
contacted = self.results_raw.get('contacted')
if dark:
for host, info in dark.items():
result['failed'][host] = info.get('msg')
if contacted:
for host, info in contacted.items():
if info.get('invocation').get('module_name') in ['raw', 'shell', 'command', 'script']:
if info.get('rc') == 0:
result['ok'][host] = info.get('stdout') + info.get('stderr')
else:
result['failed'][host] = info.get('stdout') + info.get('stderr')
else:
if info.get('failed'):
result['failed'][host] = info.get('msg')
else:
result['ok'][host] = info.get('changed')
return result
class Command(MyInventory):
"""
this is a command object for parallel execute command.
"""
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
self.results_raw = {}
def run(self, command, module_name="command", timeout=10, forks=10, pattern=''):
"""
run command from andible ad-hoc.
command : 必须是一个需要执行的命令字符串, 比如
'uname -a'
"""
data = {}
if module_name not in ["raw", "command", "shell"]:
raise CommandValueError("module_name",
"module_name must be of the 'raw, command, shell'")
hoc = Runner(module_name=module_name,
module_args=command,
timeout=timeout,
inventory=self.inventory,
pattern=pattern,
forks=forks,
)
self.results_raw = hoc.run()
@property
def result(self):
result = {}
for k, v in self.results_raw.items():
if k == 'dark':
for host, info in v.items():
result[host] = {'dark': info.get('msg')}
elif k == 'contacted':
for host, info in v.items():
result[host] = {}
if info.get('stdout'):
result[host]['stdout'] = info.get('stdout')
elif info.get('stderr'):
result[host]['stderr'] = info.get('stderr')
return result
@property
def state(self):
result = {}
if self.stdout:
result['ok'] = self.stdout
if self.stderr:
result['err'] = self.stderr
if self.dark:
result['dark'] = self.dark
return result
@property
def exec_time(self):
"""
get the command execute time.
"""
result = {}
all = self.results_raw.get("contacted")
for key, value in all.iteritems():
result[key] = {
"start": value.get("start"),
"end" : value.get("end"),
"delta": value.get("delta"),}
return result
@property
def stdout(self):
"""
get the comamnd standard output.
"""
result = {}
all = self.results_raw.get("contacted")
for key, value in all.iteritems():
result[key] = value.get("stdout")
return result
@property
def stderr(self):
"""
get the command standard error.
"""
result = {}
all = self.results_raw.get("contacted")
for key, value in all.iteritems():
if value.get("stderr") or value.get("warnings"):
result[key] = {
"stderr": value.get("stderr"),
"warnings": value.get("warnings"),}
return result
@property
def dark(self):
"""
get the dark results.
"""
return self.results_raw.get("dark")
class MyTask(MyRunner):
"""
this is a tasks object for include the common command.
"""
def __init__(self, *args, **kwargs):
super(MyTask, self).__init__(*args, **kwargs)
def push_key(self, user, key_path):
"""
push the ssh authorized key to target.
"""
module_args = 'user="%s" key="{{ lookup("file", "%s") }}" state=present' % (user, key_path)
self.run("authorized_key", module_args, become=True)
return self.results
def push_multi_key(self, **user_info):
"""
push multi key
:param user_info:
:return:
"""
ret_failed = []
ret_success = []
for user, key_path in user_info.iteritems():
ret = self.push_key(user, key_path)
if ret.get("status") == "ok":
ret_success.append(ret)
if ret.get("status") == "failed":
ret_failed.append(ret)
if ret_failed:
return {"status": "failed", "msg": ret_failed}
else:
return {"status": "success", "msg": ret_success}
def del_key(self, user, key_path):
"""
push the ssh authorized key to target.
"""
module_args = 'user="%s" key="{{ lookup("file", "%s") }}" state="absent"' % (user, key_path)
self.run("authorized_key", module_args, become=True)
return self.results
def add_user(self, username, password=''):
"""
add a host user.
"""
if password:
encrypt_pass = sha512_crypt.encrypt(password)
module_args = 'name=%s shell=/bin/bash password=%s' % (username, encrypt_pass)
else:
module_args = 'name=%s shell=/bin/bash' % username
self.run("user", module_args, become=True)
return self.results
def add_multi_user(self, **user_info):
"""
add multi user
:param user_info: keyword args
{username: password}
:return:
"""
ret_success = []
ret_failed = []
for user, password in user_info.iteritems():
ret = self.add_user(user, password)
if ret.get("status") == "ok":
ret_success.append(ret)
if ret.get("status") == "failed":
ret_failed.append(ret)
if ret_failed:
return {"status": "failed", "msg": ret_failed}
else:
return {"status": "success", "msg": ret_success}
def del_user(self, username):
"""
delete a host user.
"""
module_args = 'name=%s state=absent remove=yes move_home=yes force=yes' % username
self.run("user", module_args, become=True)
return self.results
def del_user_sudo(self, username):
"""
delete a role sudo item
:param username:
:return:
"""
module_args = "sed -i 's/^%s.*//' /etc/sudoers" % username
self.run("command", module_args, become=True)
return self.results
@staticmethod
def gen_sudo_script(role_list, sudo_list):
# receive role_list = [role1, role2] sudo_list = [sudo1, sudo2]
# return sudo_alias={'NETWORK': '/sbin/ifconfig, /ls'} sudo_user={'user1': ['NETWORK', 'SYSTEM']}
sudo_alias = {}
sudo_user = {}
for sudo in sudo_list:
sudo_alias[sudo.name] = sudo.commands
for role in role_list:
sudo_user[role.name] = ','.join(sudo_alias.keys())
sudo_j2 = get_template('jperm/role_sudo.j2')
sudo_content = sudo_j2.render(Context({"sudo_alias": sudo_alias, "sudo_user": sudo_user}))
sudo_file = NamedTemporaryFile(delete=False)
sudo_file.write(sudo_content)
sudo_file.close()
return sudo_file.name
def push_sudo_file(self, role_list, sudo_list):
"""
use template to render pushed sudoers file
:return:
"""
module_args1 = self.gen_sudo_script(role_list, sudo_list)
self.run("script", module_args1, become=True)
return self.results
class CustomAggregateStats(callbacks.AggregateStats):
"""
Holds stats about per-host activity during playbook runs.
"""
def __init__(self):
super(CustomAggregateStats, self).__init__()
self.results = []
def compute(self, runner_results, setup=False, poll=False,
ignore_errors=False):
"""
Walk through all results and increment stats.
"""
super(CustomAggregateStats, self).compute(runner_results, setup, poll,
ignore_errors)
self.results.append(runner_results)
def summarize(self, host):
"""
Return information about a particular host
"""
summarized_info = super(CustomAggregateStats, self).summarize(host)
# Adding the info I need
summarized_info['result'] = self.results
return summarized_info
class MyPlaybook(MyInventory):
"""
this is my playbook object for execute playbook.
"""
def __init__(self, *args, **kwargs):
super(MyPlaybook, self).__init__(*args, **kwargs)
def run(self, playbook_relational_path, extra_vars=None):
"""
run ansible playbook,
only surport relational path.
"""
stats = callbacks.AggregateStats()
playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
playbook_path = os.path.join(ANSIBLE_DIR, playbook_relational_path)
pb = PlayBook(
playbook=playbook_path,
stats=stats,
callbacks=playbook_cb,
runner_callbacks=runner_cb,
inventory=self.inventory,
extra_vars=extra_vars,
check=False)
self.results = pb.run()
@property
def raw_results(self):
"""
get the raw results after playbook run.
"""
return self.results
class App(MyPlaybook):
"""
this is a app object for inclue the common playbook.
"""
def __init__(self, *args, **kwargs):
super(App, self).__init__(*args, **kwargs)
if __name__ == "__main__":
# resource = {
# "group1": {
# "hosts": [{"hostname": "127.0.0.1", "port": "22", "username": "root", "password": "xxx"},],
# "vars" : {"var1": "value1", "var2": "value2"},
# },
# }
resource = [{"hostname": "127.0.0.1", "port": "22", "username": "yumaojun", "password": "yusky0902",
# "ansible_become": "yes",
# "ansible_become_method": "sudo",
# # "ansible_become_user": "root",
# "ansible_become_pass": "yusky0902",
}]
cmd = Command(resource)
print cmd.run('ls')
# resource = [{"hostname": "192.168.10.148", "port": "22", "username": "root", "password": "xxx"}]
# task = Tasks(resource)
# print task.get_host_info()
# playbook = MyPlaybook(resource)
# playbook.run('test.yml')
# print playbook.raw_results
# task = Tasks(resource)
# print task.add_user('test', 'mypass')
# print task.del_user('test')
# print task.push_key('root', '/root/.ssh/id_rsa.pub')
# print task.del_key('root', '/root/.ssh/id_rsa.pub')
# task = Tasks(resource)
# print task.add_init_users()
# print task.del_init_users()

60
jperm/models.py Normal file
View File

@@ -0,0 +1,60 @@
import datetime
from django.db import models
from jasset.models import Asset, AssetGroup
from juser.models import User, UserGroup
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 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 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=128)
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.name
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.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)

318
jperm/perm_api.py Normal file
View File

@@ -0,0 +1,318 @@
# coding: utf-8
from django.db.models.query import QuerySet
from jumpserver.api import *
import uuid
import re
from jumpserver.models import Setting
from jperm.models import PermRole, PermPush, PermRule
def get_group_user_perm(ob):
"""
ob为用户或用户组
获取用户、用户组授权的资产、资产组
return:
{asset_group': {
asset_group1: {'asset': [], 'role': [role1, role2], 'rule': [rule1, rule2]},
asset_group2: {'asset: [], 'role': [role1, role2], 'rule': [rule1, rule2]},
}
'asset':{
asset1: {'role': [role1, role2], 'rule': [rule1, rule2]},
asset2: {'role': [role1, role2], 'rule': [rule1, rule2]},
}
]},
'rule':[rule1, rule2,]
'role': {role1: {'asset': []}, 'asset_group': []}, role2: {}},
}
"""
perm = {}
if isinstance(ob, User):
rule_all = set(PermRule.objects.filter(user=ob))
for user_group in ob.group.all():
rule_all = rule_all.union(set(PermRule.objects.filter(user_group=user_group)))
elif isinstance(ob, UserGroup):
rule_all = PermRule.objects.filter(user_group=ob)
else:
rule_all = []
perm['rule'] = rule_all
perm_asset_group = perm['asset_group'] = {}
perm_asset = perm['asset'] = {}
perm_role = perm['role'] = {}
for rule in rule_all:
asset_groups = rule.asset_group.all()
assets = rule.asset.all()
perm_roles = rule.role.all()
group_assets = []
for asset_group in asset_groups:
group_assets.extend(asset_group.asset_set.all())
# 获取一个规则授权的角色和对应主机
for role in perm_roles:
if perm_role.get(role):
perm_role[role]['asset'] = perm_role[role].get('asset', set()).union(set(assets).union(set(group_assets)))
perm_role[role]['asset_group'] = perm_role[role].get('asset_group', set()).union(set(asset_groups))
else:
perm_role[role] = {'asset': set(assets).union(set(group_assets)), 'asset_group': set(asset_groups)}
# 获取一个规则用户授权的资产
for asset in assets:
if perm_asset.get(asset):
perm_asset[asset].get('role', set()).update(set(rule.role.all()))
perm_asset[asset].get('rule', set()).add(rule)
else:
perm_asset[asset] = {'role': set(rule.role.all()), 'rule': set([rule])}
# 获取一个规则用户授权的资产组
for asset_group in asset_groups:
asset_group_assets = asset_group.asset_set.all()
if perm_asset_group.get(asset_group):
perm_asset_group[asset_group].get('role', set()).update(set(rule.role.all()))
perm_asset_group[asset_group].get('rule', set()).add(rule)
else:
perm_asset_group[asset_group] = {'role': set(rule.role.all()), 'rule': set([rule]),
'asset': asset_group_assets}
# 将资产组中的资产添加到资产授权中
for asset in asset_group_assets:
if perm_asset.get(asset):
perm_asset[asset].get('role', set()).update(perm_asset_group[asset_group].get('role', set()))
perm_asset[asset].get('rule', set()).update(perm_asset_group[asset_group].get('rule', set()))
else:
perm_asset[asset] = {'role': perm_asset_group[asset_group].get('role', set()),
'rule': perm_asset_group[asset_group].get('rule', set())}
return perm
def get_group_asset_perm(ob):
"""
ob为资产或资产组
获取资产,资产组授权的用户,用户组
return:
{user_group': {
user_group1: {'user': [], 'role': [role1, role2], 'rule': [rule1, rule2]},
user_group2: {'user: [], 'role': [role1, role2], 'rule': [rule1, rule2]},
}
'user':{
user1: {'role': [role1, role2], 'rule': [rule1, rule2]},
user2: {'role': [role1, role2], 'rule': [rule1, rule2]},
}
]},
'rule':[rule1, rule2,],
}
"""
perm = {}
if isinstance(ob, Asset):
rule_all = PermRule.objects.filter(asset=ob)
elif isinstance(ob, AssetGroup):
rule_all = PermRule.objects.filter(asset_group=ob)
else:
rule_all = []
perm['rule'] = rule_all
perm_user_group = perm['user_group'] = {}
perm_user = perm['user'] = {}
for rule in rule_all:
user_groups = rule.user_group.all()
users = rule.user.all()
# 获取一个规则资产的用户
for user in users:
if perm_user.get(user):
perm_user[user].get('role', set()).update(set(rule.role.all()))
perm_user[user].get('rule', set()).add(rule)
else:
perm_user[user] = {'role': set(rule.role.all()), 'rule': set([rule])}
# 获取一个规则资产授权的用户组
for user_group in user_groups:
user_group_users = user_group.user_set.all()
if perm_user_group.get(user_group):
perm_user_group[user_group].get('role', set()).update(set(rule.role.all()))
perm_user_group[user_group].get('rule', set()).add(rule)
else:
perm_user_group[user_group] = {'role': set(rule.role.all()), 'rule': set([rule]),
'user': user_group_users}
# 将用户组中的资产添加到用户授权中
for user in user_group_users:
if perm_user.get(user):
perm_user[user].get('role', set()).update(perm_user_group[user_group].get('role', set()))
perm_user[user].get('rule', set()).update(perm_user_group[user_group].get('rule', set()))
else:
perm_user[user] = {'role': perm_user_group[user_group].get('role', set()),
'rule': perm_user_group[user_group].get('rule', set())}
return perm
def user_have_perm(user, asset):
user_perm_all = get_group_user_perm(user)
user_assets = user_perm_all.get('asset').keys()
if asset in user_assets:
return user_perm_all.get('asset').get(asset).get('role')
else:
return []
def gen_resource(ob, perm=None):
"""
ob为用户或资产列表或资产queryset, 如果同时输入用户和{'role': role1, 'asset': []},则获取用户在这些资产上的信息
生成MyInventory需要的 resource文件
"""
res = []
if isinstance(ob, dict):
role = ob.get('role')
asset_r = ob.get('asset')
user = ob.get('user')
if not perm:
perm = get_group_user_perm(user)
if role:
roles = perm.get('role', {}).keys() # 获取用户所有授权角色
if role not in roles:
return {}
role_assets_all = perm.get('role').get(role).get('asset') # 获取用户该角色所有授权主机
assets = set(role_assets_all) & set(asset_r) # 获取用户提交中合法的主机
for asset in assets:
asset_info = get_asset_info(asset)
role_key = get_role_key(user, role)
info = {'hostname': asset.hostname,
'ip': asset.ip,
'port': asset_info.get('port', 22),
'ansible_ssh_private_key_file': role_key,
'username': role.name,
# 'password': CRYPTOR.decrypt(role.password)
}
if os.path.isfile(role_key):
info['ssh_key'] = role_key
res.append(info)
else:
for asset, asset_info in perm.get('asset').items():
if asset not in asset_r:
continue
asset_info = get_asset_info(asset)
try:
role = sorted(list(perm.get('asset').get(asset).get('role')))[0]
except IndexError:
continue
role_key = get_role_key(user, role)
info = {'hostname': asset.hostname,
'ip': asset.ip,
'port': asset_info.get('port', 22),
'username': role.name,
'password': CRYPTOR.decrypt(role.password),
}
if os.path.isfile(role_key):
info['ssh_key'] = role_key
res.append(info)
elif isinstance(ob, User):
if not perm:
perm = get_group_user_perm(ob)
for asset, asset_info in perm.get('asset').items():
asset_info = get_asset_info(asset)
info = {'hostname': asset.hostname, 'ip': asset.ip, 'port': asset_info.get('port', 22)}
try:
role = sorted(list(perm.get('asset').get(asset).get('role')))[0]
except IndexError:
continue
info['username'] = role.name
info['password'] = CRYPTOR.decrypt(role.password)
role_key = get_role_key(ob, role)
if os.path.isfile(role_key):
info['ssh_key'] = role_key
res.append(info)
elif isinstance(ob, (list, QuerySet)):
for asset in ob:
info = get_asset_info(asset)
res.append(info)
logger.debug('生成res: %s' % res)
return res
def get_object_list(model, id_list):
"""根据id列表获取对象列表"""
object_list = []
for object_id in id_list:
if object_id:
object_list.extend(model.objects.filter(id=int(object_id)))
return object_list
def get_role_info(role_id, type="all"):
"""
获取role对应的一些信息
:return: 返回值 均为对象列表
"""
# 获取role对应的授权规则
role_obj = PermRole.objects.get(id=role_id)
rule_push_obj = role_obj.perm_rule.all()
# 获取role 对应的用户 和 用户组
# 获取role 对应的主机 和主机组
users_obj = []
assets_obj = []
user_groups_obj = []
asset_groups_obj = []
for push in rule_push_obj:
for user in push.user.all():
users_obj.append(user)
for asset in push.asset.all():
assets_obj.append(asset)
for user_group in push.user_group.all():
user_groups_obj.append(user_group)
for asset_group in push.asset_group.all():
asset_groups_obj.append(asset_group)
if type == "all":
return {"rules": set(rule_push_obj),
"users": set(users_obj),
"user_groups": set(user_groups_obj),
"assets": set(assets_obj),
"asset_groups": set(asset_groups_obj),
}
elif type == "rule":
return set(rule_push_obj)
elif type == "user":
return set(users_obj)
elif type == "user_group":
return set(user_groups_obj)
elif type == "asset":
return set(assets_obj)
elif type == "asset_group":
return set(asset_groups_obj)
else:
return u"不支持的查询"
def get_role_push_host(role):
"""
asset_pushed: {'success': push.success, 'key': push.is_public_key, 'password': push.is_password,
'result': push.result}
asset_no_push: set(asset1, asset2)
"""
# 计算该role 所有push记录 总共推送的主机
pushs = PermPush.objects.filter(role=role)
asset_all = Asset.objects.all()
asset_pushed = {}
for push in pushs:
asset_pushed[push.asset] = {'success': push.success, 'key': push.is_public_key, 'password': push.is_password,
'result': push.result}
asset_no_push = set(asset_all) - set(asset_pushed.keys())
return asset_pushed, asset_no_push
if __name__ == "__main__":
print get_role_info(1)

View File

@@ -0,0 +1,12 @@
---
- hosts: 'add_users_group'
gather_facts: no
tasks:
- name: add SA user
command: uname -a

9
jperm/playbooks/test.yml Normal file
View File

@@ -0,0 +1,9 @@
---
- hosts: test
gather_facts: no
tasks:
- name: just for test
command: uname -a

3
jperm/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

22
jperm/urls.py Normal file
View File

@@ -0,0 +1,22 @@
from django.conf.urls import patterns, include, url
from jperm.views import *
urlpatterns = patterns('jperm.views',
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'),
)

80
jperm/utils.py Normal file
View File

@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
import os.path
import shutil
from paramiko import SSHException
from paramiko.rsakey import RSAKey
from jumpserver.api import mkdir
from uuid import uuid4
from jumpserver.api import CRYPTOR
from jumpserver.api import logger
from jumpserver.settings import KEY_DIR
def get_rand_pass():
"""
get a reandom password.
"""
CRYPTOR.gen_rand_pass(20)
def updates_dict(*args):
"""
surport update multi dict
"""
result = {}
for d in args:
result.update(d)
return result
def gen_keys(key="", key_path_dir=""):
"""
在KEY_DIR下创建一个 uuid命名的目录
并且在该目录下 生产一对秘钥
:return: 返回目录名(uuid)
"""
key_basename = "key-" + uuid4().hex
if not key_path_dir:
key_path_dir = os.path.join(KEY_DIR, 'role_key', key_basename)
private_key = os.path.join(key_path_dir, 'id_rsa')
public_key = os.path.join(key_path_dir, 'id_rsa.pub')
mkdir(key_path_dir, mode=0755)
if not key:
key = RSAKey.generate(2048)
key.write_private_key_file(private_key)
else:
key_file = os.path.join(key_path_dir, 'id_rsa')
with open(key_file, 'w') as f:
f.write(key)
f.close()
with open(key_file) as f:
try:
key = RSAKey.from_private_key(f)
except SSHException, e:
shutil.rmtree(key_path_dir, ignore_errors=True)
raise SSHException(e)
os.chmod(private_key, 0644)
with open(public_key, 'w') as content_file:
for data in [key.get_name(),
" ",
key.get_base64(),
" %s@%s" % ("jumpserver", os.uname()[1])]:
content_file.write(data)
return key_path_dir
def trans_all(str):
if str.strip().lower() == "all":
return str.upper()
else:
return str
if __name__ == "__main__":
print gen_keys()

741
jperm/views.py Normal file
View File

@@ -0,0 +1,741 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db.models import Q
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
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, trans_all
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.api import logger
#logger = set_log(LOG_LEVEL, filename='jumpserver_perm.log')
@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', '')
if rule_id:
rules_list = rules_list.filter(id=rule_id)
if keyword:
rules_list = rules_list.filter(Q(name=keyword))
rules_list, p, rules, page_range, current_page, show_first, show_end = pages(rules_list, request)
return my_render('jperm/perm_rule_list.html', locals(), request)
@require_role('admin')
def perm_rule_detail(request):
"""
rule detail page
授权详情
"""
# 渲染数据
header_title, path1, path2 = "授权规则", "规则管理", "规则详情"
# 根据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()]
# 渲染数据
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)
return my_render('jperm/perm_rule_detail.html', locals(), request)
def perm_rule_add(request):
"""
add rule page
添加授权
"""
# 渲染数据
header_title, path1, path2 = "授权规则", "规则管理", "添加规则"
# 渲染数据, 获取所有 用户,用户组,资产,资产组,用户角色, 用于添加授权规则
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':
# 获取用户选择的 用户,用户组,资产,资产组,用户角色
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)
@require_role('admin')
def perm_rule_edit(request):
"""
edit rule page
"""
# 渲染数据
header_title, path1, path2 = "授权规则", "规则管理", "添加规则"
# 根据rule_id 取得rule对象
rule_id = request.GET.get("id")
rule = get_object(PermRule, id=rule_id)
# 渲染数据, 获取所选的rule对象
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', [])
try:
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.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
except ServerError, e:
error = e
return my_render('jperm/perm_rule_edit.html', locals(), request)
@require_role('admin')
def perm_rule_delete(request):
"""
use to delete rule
:param request:
:return:
"""
if request.method == 'POST':
# 根据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", "").strip()
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 get_object(PermRole, name=name):
raise ServerError(u'已经存在该用户 %s' % name)
if name == "root":
raise ServerError(u'禁止使用root用户作为系统用户这样非常危险')
default = get_object(Setting, name='default')
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:
try:
key_path = gen_keys(key=key_content)
except SSHException, e:
raise ServerError(e)
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'))
except ServerError, e:
error = e
return my_render('jperm/perm_role_add.html', locals(), request)
@require_role('admin')
def perm_role_delete(request):
"""
delete role page
"""
if request.method == "GET":
try:
# 获取参数删除的role对象
role_id = request.GET.get("id")
role = get_object(PermRole, id=role_id)
if not role:
logger.warning(u"Delete Role: role_id %s not exist" % role_id)
raise ServerError(u"role_id %s 无数据记录" % role_id)
# 删除推送到主机上的role
filter_type = request.GET.get("filter_type")
print filter_type
if filter_type:
if filter_type == "recycle_assets":
recycle_assets = [push.asset for push in role.perm_push.all() if push.success]
print recycle_assets
recycle_assets_ip = ','.join([asset.ip for asset in recycle_assets])
return HttpResponse(recycle_assets_ip)
else:
return HttpResponse("no such filter_type: %s" % filter_type)
else:
return HttpResponse("filter_type: ?")
except ServerError, e:
return HttpResponse(e)
if request.method == "POST":
try:
# 获取参数删除的role对象
role_id = request.POST.get("id")
role = get_object(PermRole, id=role_id)
if not role:
logger.warning(u"Delete Role: role_id %s not exist" % role_id)
raise ServerError(u"role_id %s 无数据记录" % 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)
try:
msg_del_user = task.del_user(get_object(PermRole, id=role_id).name)
msg_del_sudo = task.del_user_sudo(get_object(PermRole, id=role_id).name)
except Exception, e:
logger.warning(u"Recycle Role failed: %s" % e)
raise ServerError(u"回收已推送的系统用户失败: %s" % e)
logger.info(u"delete role %s - execute delete user: %s" % (role.name, msg_del_user))
logger.info(u"delete role %s - execute delete sudo: %s" % (role.name, msg_del_sudo))
# TODO: 判断返回结果,处理异常
# 删除存储的秘钥,以及目录
try:
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)
except OSError, e:
logger.warning(u"Delete Role: delete key error, %s" % e)
raise ServerError(u"删除系统用户key失败: %s" % e)
logger.info(u"delete role %s - delete role key directory: %s" % (role.name, role_key))
# 数据库里删除记录
role.delete()
return HttpResponse(u"删除系统用户: %s" % role.name)
except ServerError, e:
return HttpResponseBadRequest(u"删除失败, 原因: %s" % e)
return HttpResponseNotAllowed(u"仅支持POST")
@require_role('admin')
def perm_role_detail(request):
"""
the role detail page
the role_info data like:
{'asset_groups': [],
'assets': [<Asset: 192.168.10.148>],
'rules': [<PermRule: PermRule object>],
'': [],
'': [<User: user1>]}
"""
# 渲染数据
header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户详情"
try:
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)
# 渲染数据
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_role('admin')
def perm_role_edit(request):
"""
edit role page
"""
# 渲染数据
header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户编辑"
# 渲染数据
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_name == "root":
raise ServerError(u'禁止使用root用户作为系统用户这样非常危险')
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_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 = {}
# 因为要先建立用户而push key是在 password也完成的情况下的 可选项
# 1. 以秘钥 方式推送角色
if key_push:
ret["pass_push"] = task.add_user(role.name)
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 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:
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命令", "别名管理", "添加别名"
try:
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()
if not name or not commands:
raise ServerError(u"sudo name 和 commands是必填项!")
pattern = re.compile(r'[\n,\r]')
deal_space_commands = list_drop_str(pattern.split(commands), u'')
deal_all_commands = map(trans_all, deal_space_commands)
commands = ', '.join(deal_all_commands)
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
except ServerError, e:
error = e
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)
try:
if request.method == "POST":
name = request.POST.get("sudo_name").upper()
commands = request.POST.get("sudo_commands")
comment = request.POST.get("sudo_comment")
if not name or not commands:
raise ServerError(u"sudo name 和 commands是必填项!")
pattern = re.compile(r'[\n,\r]')
deal_space_commands = list_drop_str(pattern.split(commands), u'')
deal_all_commands = map(trans_all, deal_space_commands)
commands = ', '.join(deal_all_commands).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
except ServerError, e:
error = e
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:
return HttpResponse(u"不支持该操作")
@require_role('admin')
def perm_role_recycle(request):
role_id = request.GET.get('role_id')
asset_ids = request.GET.get('asset_id').split(',')
# 仅有推送的角色才回收
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)
try:
msg_del_user = task.del_user(get_object(PermRole, id=role_id).name)
msg_del_sudo = task.del_user_sudo(get_object(PermRole, id=role_id).name)
logger.info("recycle user msg: %s" % msg_del_user)
logger.info("recycle sudo msg: %s" % msg_del_sudo)
except Exception, e:
logger.warning("Recycle Role failed: %s" % e)
raise ServerError(u"回收已推送的系统用户失败: %s" % e)
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()
return HttpResponse('删除成功')
@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(u'获取授权系统用户: ' + ','.join([i.name for i in role]))
return HttpResponse(','.join([i.name for i in role]))
else:
roles = get_group_user_perm(request.user).get('role').keys()
return HttpResponse(','.join(i.name for i in roles))
return HttpResponse('error')

View File

@@ -1,16 +1,22 @@
#coding:utf-8
[base]
url = http://127.0.0.1
key = 941enj9neshd1wes
ip = 0.0.0.0
port = 8000
log = debug
[db]
host = 127.0.0.1
port = 3306
user = root
password = redhat
db = jumpserver
user = jumpserver
password = mysql234
database = jumpserver
[jumpserver]
key = 88aaaf7ffe3c6c04
ldap_host = ldap://127.0.0.1:389
ldap_base_dn = dc=yolu,dc=com
admin_cn = cn=admin,dc=yolu,dc=com
admin_pass = VNLqNCjpNBIetEoCA2h3
web_socket_host = 172.10.10.9:3000
[mail]
mail_enable = 1
email_host =
email_port = 587
email_host_user =
email_host_password =
email_use_tls = True
email_use_ssl = False

View File

@@ -1,310 +0,0 @@
#!/usr/bin/python
# coding: utf-8
import os
import sys
import subprocess
import struct
import fcntl
import termios
import signal
import re
import time
from Crypto.Cipher import AES
from binascii import b2a_hex, a2b_hex
import ConfigParser
import paramiko
import pxssh
import pexpect
cur_dir = os.path.abspath(os.path.dirname(__file__))
sys.path.append('%s/webroot/AutoSa/' % cur_dir)
os.environ['DJANGO_SETTINGS_MODULE'] = 'AutoSa.settings'
import django
django.setup()
from UserManage.models import User, Logs, Pid
from Assets.models import Assets
cf = ConfigParser.ConfigParser()
cf.read('%s/jumpserver.conf' % cur_dir)
db_host = cf.get('db', 'host')
db_port = cf.getint('db', 'port')
db_user = cf.get('db', 'user')
db_password = cf.get('db', 'password')
db_db = cf.get('db', 'db')
log_dir = os.path.join(cur_dir, 'logs')
key = cf.get('jumpserver', 'key')
class PyCrypt(object):
"""It's 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
count = len(text)
if count < length:
add = (length - count)
text += ('\0' * add)
elif count > length:
add = (length - (count % length))
text += ('\0' * add)
ciphertext = cryptor.encrypt(text)
return b2a_hex(ciphertext)
def decrypt(self, text):
cryptor = AES.new(self.key, self.mode, b'0000000000000000')
plain_text = cryptor.decrypt(a2b_hex(text))
return plain_text.rstrip('\0')
def sigwinch_passthrough(sig, data):
"""This function use to set the window size of the terminal!"""
winsize = getwinsize()
try:
foo.setwinsize(winsize[0], winsize[1])
except:
pass
def getwinsize():
"""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]
def run_cmd(cmd):
"""run command and return stdout"""
pipe = subprocess.Popen(cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if pipe.stdout:
stdout = pipe.stdout.read().strip()
pipe.wait()
return stdout
if pipe.stderr:
stderr = pipe.stderr.read()
pipe.wait()
return stderr
def connect(host, port, user, password):
"""Use pexpect module to connect other server."""
log_date_dir = '%s/%s' % (log_dir, time.strftime('%Y%m%d'))
if not os.path.isdir(log_date_dir):
os.mkdir(log_date_dir)
os.chmod(log_date_dir, 0777)
structtime_start = time.localtime()
datetime_start = time.strftime('%Y%m%d%H%M%S', structtime_start)
logtime_start = time.strftime('%Y/%m/%d %H:%M:%S', structtime_start)
timestamp_start = int(time.mktime(structtime_start))
logfile_name = "%s/%s_%s_%s" % (log_date_dir, host, user, datetime_start)
try:
global foo
foo = pxssh.pxssh()
foo.login(host, user, password, port=port, auto_prompt_reset=False)
log = Logs(user=user, host=host, logfile=logfile_name, start_time=timestamp_start, ppid=os.getpid()) # 日志信息记录到数据库
log.save()
pid = Pid(ppid=os.getpid(), cpid=foo.pid, start_time=timestamp_start, logid=log.id)
pid.save()
logfile = open(logfile_name, 'a') # 记录日志文件
logfile.write('\nDateTime:%s' % logtime_start)
foo.logfile = logfile
foo.sendline('')
signal.signal(signal.SIGWINCH, sigwinch_passthrough)
size = getwinsize()
foo.setwinsize(size[0], size[1])
foo.interact(escape_character=chr(28)) # 进入交互模式
logfile.write('\nEndTime: %s' % time.strftime('%Y/%m/%d %H:%M:%S'))
log.finish = 1
log.end_time = int(time.time())
log.save()
except pxssh.ExceptionPxssh as e:
print('登录失败: %s' % e)
except pexpect.EOF:
print('登录失败: Host refuse')
except KeyboardInterrupt:
foo.logout()
log.finish = 1
log.end_time = int(time.time())
log.save()
def ip_all_select(username):
"""select all the server of the user can control."""
ip_all = []
ip_all_dict = {}
try:
user = User.objects.get(username=username)
except:
return (['error'], {'error':"Don't Use Root To Do That or User isn't Exist."})
all_assets_user = user.assetsuser_set.all()
for assets_user in all_assets_user:
ip_all.append(assets_user.aid.ip)
ip_all_dict[assets_user.aid.ip] = assets_user.aid.comment
return ip_all, ip_all_dict
def sth_select(username='', ip=''):
"""if username: return password elif ip return port"""
if username:
user = User.objects.get(username=username)
ldap_password = user.ldap_password
return ldap_password
if ip:
asset = Assets.objects.get(ip=ip)
port = asset.port
return port
return None
def remote_exec_cmd(host, user, cmd):
jm = PyCrypt(key)
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
port = sth_select(ip=host)
password = jm.decrypt(sth_select(username=username))
try:
ssh.connect(host, port, user, password)
except paramiko.AuthenticationException:
print 'Password Error .'
return None
stdin, stdout, stderr = ssh.exec_command(cmd)
print '\033[32m' + '#'*15 + ' ' + host + ' ' + '#'*15 + '\n' + '\033[0m'
output = stdout.read()
error = stderr.read()
if output:
print output
if error:
print error
print '\033[32m' + '#'*15 + ' End result ' + '#'*15 + '\n' + '\033[0m'
ssh.close()
def match_ip(all_ip, string):
ip_matched = []
pattern = re.compile(r'%s' % string)
for ip in all_ip:
if pattern.search(ip):
ip_matched.append(ip)
return ip_matched
def print_prompt():
print """
\033[1;32m### Welcome Use JumpServer To Login. ### \033[0m
1) Type \033[32mIP ADDRESS\033[0m To Login.
2) Type \033[32mP/p\033[0m To Print The Servers You Available.
3) Type \033[32mE/e\033[0m To Execute Command On Several Servers.
4) Type \033[32mQ/q\033[0m To Quit.
"""
def print_your_server(username):
ip_all, ip_all_dict = ip_all_select(username)
for ip in ip_all:
if ip_all_dict[ip]:
print "%s -- %s" % (ip, ip_all_dict[ip])
else:
print ip
def exec_cmd_servers(username):
print '\nInput the \033[32mHost IP(s)\033[0m,Separated by Commas, q/Q to Quit.\n'
while True:
hosts = raw_input('\033[1;32mip(s)>: \033[0m')
if hosts in ['q', 'Q']:
break
hosts = hosts.split(',')
hosts.append('')
hosts = list(set(hosts))
hosts.remove('')
ip_all, ip_all_dict = ip_all_select(username)
no_perm = set(hosts)-set(ip_all)
if no_perm:
print "You have NO PERMISSION on %s..." % list(no_perm)
continue
print '\nInput the \033[32mCommand\033[0m , The command will be Execute on servers, q/Q to quit.\n'
while True:
cmd = raw_input('\033[1;32mCmd(s): \033[0m')
if cmd in ['q', '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))
for host in hosts:
remote_exec_cmd(host, username, cmd)
def connect_one(username, option):
ip_input = option.strip()
ip_all, ip_all_dict = ip_all_select(username)
ip_matched = match_ip(ip_all, ip_input)
ip_len = len(ip_matched)
ip = ''
if ip_len >= 1:
if ip_len == 1:
ip = ip_matched[0]
else:
if ip_input in ip_matched:
ip = ip_input
else:
for one_ip in ip_matched:
print one_ip
if ip:
password = jm.decrypt(sth_select(username=username))
port = sth_select(ip=ip)
print "Connecting %s ..." % ip
connect(ip, port, username, password)
else:
print '\033[31mNo permision .\033[0m'
if __name__ == '__main__':
username = run_cmd('whoami')
jm = PyCrypt(key)
print_prompt()
try:
while True:
try:
option = raw_input("\033[1;32mOpt or IP>:\033[0m ")
except EOFError:
print
continue
if option in ['P', 'p']:
print_your_server(username)
continue
elif option in ['e', 'E']:
exec_cmd_servers(username)
elif option in ['q', 'Q']:
sys.exit()
else:
connect_one(username, option)
#except (BaseException, Exception):
except IndexError:
print "Exit."
sys.exit()

511
jumpserver/api.py Normal file
View File

@@ -0,0 +1,511 @@
# coding: utf-8
import os, sys, time, re
from Crypto.Cipher import AES
import crypt
import pwd
from binascii import b2a_hex, a2b_hex
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 django.core.mail import send_mail
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['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['username'] = asset.username
info['password'] = CRYPTOR.decrypt(asset.password)
try:
info['port'] = int(asset.port)
except TypeError:
info['port'] = int(default.field2)
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
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)
def pages(post_objects, request):
"""
page public function , return page's object tuple
分页公用函数,返回分页的对象元组
"""
paginator = Paginator(post_objects, 20)
try:
current_page = int(request.GET.get('page', '1'))
except ValueError:
current_page = 1
page_range = page_list_return(len(paginator.page_range), current_page)
try:
page_objects = paginator.page(current_page)
except (EmptyPage, InvalidPage):
page_objects = paginator.page(paginator.num_pages)
if current_page >= 5:
show_first = 1
else:
show_first = 0
if current_page <= (len(paginator.page_range) - 3):
show_end = 1
else:
show_end = 0
# 所有对象, 分页器, 本页对象, 所有页码, 本页页码,是否显示第一页,是否显示最后一页
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.
加密类
"""
def __init__(self, key):
self.key = key
self.mode = AES.MODE_CBC
@staticmethod
def gen_rand_pass(length=16, 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(passwd)
except TypeError:
raise ServerError('Encrypt password error, TYpe error.')
add = (length - (count % length))
passwd += ('\0' * add)
cipher_text = cryptor.encrypt(passwd)
return b2a_hex(cipher_text)
def decrypt(self, text):
"""
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:
raise ServerError('Decrypt password error, TYpe error.')
return plain_text.rstrip('\0')
class ServerError(Exception):
"""
self define exception
自定义异常
"""
pass
def get_object(model, **kwargs):
"""
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_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 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 get_session_user_dept(request):
"""
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_role
def get_session_user_info(request):
"""
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):
"""
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
def api_user(request):
hosts = Log.objects.filter(is_finished=0).count()
users = Log.objects.filter(is_finished=0).values('user').distinct().count()
ret = {'users': users, 'hosts': hosts}
json_data = json.dumps(ret)
return HttpResponse(json_data)
def view_splitter(request, su=None, adm=None):
"""
for different user use different view
视图分页器
"""
if is_role_request(request, 'super'):
return su(request)
elif is_role_request(request, 'admin'):
return adm(request)
else:
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 = []
for group in dept_user_groups:
user_group_ids.append(str(group.id))
if not set(user_group).issubset(set(user_group_ids)):
return False
if user:
dept_users = dept.user_set.all()
user_ids = []
for dept_user in dept_users:
user_ids.append(str(dept_user.id))
if not set(user).issubset(set(user_ids)):
return False
if asset_group:
dept_asset_groups = dept.bisgroup_set.all()
asset_group_ids = []
for group in dept_asset_groups:
asset_group_ids.append(str(group.id))
if not set(asset_group).issubset(set(asset_group_ids)):
return False
if asset:
dept_assets = dept.asset_set.all()
asset_ids = []
for dept_asset in dept_assets:
asset_ids.append(str(dept_asset.id))
if not set(asset).issubset(set(asset_ids)):
return False
return True
def verify(request, user_group=None, user=None, asset_group=None, asset=None, edept=None):
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_groups = []
for user_group_id in user_group:
user_groups.extend(UserGroup.objects.filter(id=user_group_id))
if not set(user_groups).issubset(set(dept_user_groups)):
return False
if user:
dept_users = dept.user_set.all()
users = []
for user_id in user:
users.extend(User.objects.filter(id=user_id))
if not set(users).issubset(set(dept_users)):
return False
if asset_group:
dept_asset_groups = dept.bisgroup_set.all()
asset_group_ids = []
for group in dept_asset_groups:
asset_group_ids.append(str(group.id))
if not set(asset_group).issubset(set(asset_group_ids)):
return False
if asset:
dept_assets = dept.asset_set.all()
asset_ids = []
for a in dept_assets:
asset_ids.append(str(a.id))
print asset, asset_ids
if not set(asset).issubset(set(asset_ids)):
return False
return True
def bash(cmd):
"""
run a bash shell command
执行bash命令
"""
return subprocess.call(cmd, shell=True)
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)
os.chmod(dir_name, mode)
if username:
chown(dir_name, username)
def http_success(request, msg):
return render_to_response('success.html', locals())
def http_error(request, emg):
message = emg
return render_to_response('error.html', locals())
def my_render(template, data, request):
return render_to_response(template, data, context_instance=RequestContext(request))
def get_tmp_dir():
seed = uuid.uuid4().hex[:4]
dir_name = os.path.join('/tmp', '%s-%s' % (datetime.datetime.now().strftime('%Y%m%d-%H%M%S'), seed))
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)

View File

@@ -0,0 +1,25 @@
from juser.models import User
from jasset.models import Asset
from jumpserver.api import *
def name_proc(request):
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,
'session_role_id': role_id,
'user_total_num': user_total_num,
'user_active_num': user_active_num,
'host_total_num': host_total_num,
'host_active_num': host_active_num,
}
return info_dic

18
jumpserver/models.py Normal file
View File

@@ -0,0 +1,18 @@
# coding: utf-8
from django.db import models
class Setting(models.Model):
name = models.CharField(max_length=100)
field1 = models.CharField(max_length=100, null=True, blank=True)
field2 = models.CharField(max_length=100, null=True, blank=True)
field3 = models.CharField(max_length=100, null=True, blank=True)
field4 = models.CharField(max_length=100, null=True, blank=True)
field5 = models.CharField(max_length=100, null=True, blank=True)
class Meta:
db_table = u'setting'
def __unicode__(self):
return self.name

163
jumpserver/settings.py Normal file
View File

@@ -0,0 +1,163 @@
"""
Django settings for jumpserver project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
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.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')
try:
EMAIL_USE_SSL = config.getboolean('mail', 'email_use_ssl')
except ConfigParser.NoOptionError:
EMAIL_USE_SSL = False
EMAIL_BACKEND = 'django_smtp_ssl.SSLEmailBackend' if EMAIL_USE_SSL else 'django.core.mail.backends.smtp.EmailBackend'
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')
IP = config.get('base', 'ip')
PORT = config.get('base', 'port')
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '!%=t81uof5rhmtpi&(zr=q^fah#$enny-c@mswz49l42j0o49-'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = ['0.0.0.0/8']
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'django_crontab',
'bootstrapform',
'jumpserver',
'juser',
'jasset',
'jperm',
'jlog',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
# 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'jumpserver.urls'
WSGI_APPLICATION = 'jumpserver.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': DB_DATABASE,
'USER': DB_USER,
'PASSWORD': DB_PASSWORD,
'HOST': DB_HOST,
'PORT': DB_PORT,
}
}
# 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',
'django.core.context_processors.i18n',
'django.core.context_processors.media',
'django.core.context_processors.static',
'django.core.context_processors.tz',
'django.contrib.messages.context_processors.messages',
'jumpserver.context_processors.name_proc',
)
TEMPLATE_DIRS = (
os.path.join(BASE_DIR, 'templates'),
)
# STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
BOOTSTRAP_COLUMN_COUNT = 10
CRONJOBS = [
('0 1 * * *', 'jasset.asset_api.asset_ansible_update_all'),
('*/10 * * * *', 'jlog.log_api.kill_invalid_connection'),
]

48
jumpserver/tasks.py Normal file
View File

@@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
from ansible.playbook import PlayBook
from ansible import callbacks, utils
def playbook_run(inventory, playbook, default_user=None, default_port=None, default_pri_key_path=None):
stats = callbacks.AggregateStats()
playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
# run the playbook
print default_user, default_port, default_pri_key_path, inventory, playbook
if default_user and default_port and default_pri_key_path:
playbook = PlayBook(host_list=inventory,
playbook=playbook,
forks=5,
remote_user=default_user,
remote_port=default_port,
private_key_file=default_pri_key_path,
callbacks=playbook_cb,
runner_callbacks=runner_cb,
stats=stats,
become=True,
become_user='root')
else:
playbook = PlayBook(host_list=inventory,
playbook=playbook,
forks=5,
callbacks=playbook_cb,
runner_callbacks=runner_cb,
stats=stats,
become=True,
become_user='root')
results = playbook.run()
print results
results_r = {'unreachable': [], 'failures': [], 'success': []}
for hostname, result in results.items():
if result.get('unreachable', 2):
results_r['unreachable'].append(hostname)
print "%s >>> unreachable" % hostname
elif result.get('failures', 2):
results_r['failures'].append(hostname)
print "%s >>> Failed" % hostname
else:
results_r['success'].append(hostname)
print "%s >>> Success" % hostname
return results_r

View File

@@ -0,0 +1,306 @@
# coding: utf-8
import re
import ast
import time
from django import template
from jperm.models import PermPush
from jumpserver.api import *
from jperm.perm_api import get_group_user_perm
register = template.Library()
@register.filter(name='int2str')
def int2str(value):
"""
int 转换为 str
"""
return str(value)
@register.filter(name='get_role')
def get_role(user_id):
"""
根据用户id获取用户权限
"""
user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'}
user = get_object(User, id=user_id)
if user:
return user_role.get(str(user.role), u"普通用户")
else:
return u"普通用户"
@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='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='user_asset_group_count')
def user_asset_group_count(user):
"""
返回用户权限主机组的数量
"""
return len(user.asset_group.all())
@register.filter(name='bool2str')
def bool2str(value):
if value:
return u''
else:
return u''
@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='to_name')
def to_name(user_id):
"""user id 转位用户名称"""
try:
user = User.objects.filter(id=int(user_id))
if user:
user = user[0]
return user.name
except:
return '非法用户'
@register.filter(name='to_role_name')
def to_role_name(role_id):
"""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='result2bool')
def result2bool(result=''):
"""将结果定向为结果"""
result = eval(result)
unreachable = result.get('unreachable', [])
failures = result.get('failures', [])
if unreachable or failures:
return '<b style="color: red">失败</b>'
else:
return '<b style="color: green">成功</b>'
@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='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()
return names
@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 '%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 []
@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
@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 = disk_info
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

20
jumpserver/urls.py Normal file
View File

@@ -0,0 +1,20 @@
from django.conf.urls import patterns, include, url
urlpatterns = patterns('jumpserver.views',
# Examples:
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')),
)

360
jumpserver/views.py Normal file
View File

@@ -0,0 +1,360 @@
# 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 django.http import HttpResponse
# from jperm.models import Apply
import paramiko
from jumpserver.api import *
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)
date_li, date_str = [], []
for i in range(0, num):
today = today-oneday
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(x, y, z):
pass
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:
pass
return data_li
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_role_request(request, 'user'):
return index_cu(request)
elif is_role_request(request, 'super'):
# dashboard 显示汇总
users = User.objects.all()
hosts = Asset.objects.all()
online = Log.objects.filter(is_finished=0)
online_host = online.values('host').distinct()
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]
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
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
# 一周top5
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']
# 最后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]
return render_to_response('index.html', locals(), context_instance=RequestContext(request))
def skin_config(request):
return render_to_response('skin_config.html')
def is_latest():
node = uuid.getnode()
jsn = uuid.UUID(int=node).hex[-12:]
with open(os.path.join(BASE_DIR, 'version')) as f:
current_version = f.read()
lastest_version = urllib.urlopen('http://www.jumpserver.org/lastest_version.html?jsn=%s' % jsn).read().strip()
if current_version != lastest_version:
pass
@defend_attack
def Login(request):
"""登录界面"""
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')
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:
error = '用户未激活'
else:
error = '用户名或密码错误'
else:
error = '用户名或密码错误'
return render_to_response('login.html', {'error': error})
@require_role('user')
def Logout(request):
logout(request)
return HttpResponseRedirect(reverse('index'))
@require_role('admin')
def setting(request):
header_title, path1 = '项目设置', '设置'
setting_default = get_object(Setting, name='default')
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)
@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))
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]))
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, '/tmp'), 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 <br> 上传失败: [ %s ] <br>上传成功 [ %s ]' % (upload_dir,
', '.join(ret.get('failed').keys()),
', '.join(ret.get('ok').keys()))
return HttpResponse(error, status=500)
msg = u'上传目录: %s <br> 传送成功 [ %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))
@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/exec?role=%s' % (role)
return my_render('exec_cmd.html', locals(), request)
@require_role('user')
def web_terminal(request):
asset_id = request.GET.get('id')
role_name = request.GET.get('role')
asset = get_object(Asset, id=asset_id)
if asset:
hostname = asset.hostname
return render_to_response('jlog/web_terminal.html', locals())

View File

@@ -1,16 +1,14 @@
"""
WSGI config for AutoSa project.
WSGI config for jumpserver project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/howto/deployment/wsgi/
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
import sys
sys.path.append('/opt/jumpserver/webroot/AutoSa')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "AutoSa.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()

0
logs/test.log → juser/__init__.py Executable file → Normal file
View File

3
juser/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

54
juser/models.py Normal file
View File

@@ -0,0 +1,54 @@
# coding: utf-8
from django.db import models
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)
comment = models.CharField(max_length=160, blank=True, null=True)
def __unicode__(self):
return self.name
class User(AbstractUser):
USER_ROLE_CHOICES = (
('SU', 'SuperUser'),
('GA', 'GroupAdmin'),
('CU', 'CommonUser'),
)
name = models.CharField(max_length=80)
uuid = models.CharField(max_length=100)
role = models.CharField(max_length=2, choices=USER_ROLE_CHOICES, default='CU')
group = models.ManyToManyField(UserGroup)
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)

25
juser/urls.py Normal file
View File

@@ -0,0 +1,25 @@
from django.conf.urls import patterns, include, url
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')),
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'),
)

199
juser/user_api.py Normal file
View File

@@ -0,0 +1,199 @@
# coding: utf-8
from Crypto.PublicKey import RSA
from subprocess import call
from juser.models import AdminGroup
from jumpserver.api import *
from jumpserver.settings import BASE_DIR, EMAIL_HOST_USER as MAIL_FROM
def group_add_user(group, user_id=None, username=None):
"""
用户组中添加用户
UserGroup Add a user
"""
if user_id:
user = get_object(User, id=user_id)
else:
user = get_object(User, username=username)
if user:
group.user_set.add(user)
def db_add_group(**kwargs):
"""
add a user group in database
数据库中添加用户组
"""
name = kwargs.get('name')
group = get_object(UserGroup, name=name)
users = kwargs.pop('users_id')
if not group:
group = UserGroup(**kwargs)
group.save()
for user_id in users:
group_add_user(group, user_id)
def group_update_member(group_id, users_id_list):
"""
user group update member
用户组更新成员
"""
group = get_object(UserGroup, id=group_id)
if group:
group.user_set.clear()
for user_id in users_id_list:
user = get_object(UserGroup, id=user_id)
if isinstance(user, UserGroup):
group.user_set.add(user)
def db_add_user(**kwargs):
"""
add a user in database
数据库中添加用户
"""
groups_post = kwargs.pop('groups')
admin_groups = kwargs.pop('admin_groups')
role = kwargs.get('role', 'CU')
user = User(**kwargs)
user.set_password(kwargs.get('password'))
user.save()
if groups_post:
group_select = []
for group_id in groups_post:
group = UserGroup.objects.filter(id=group_id)
group_select.extend(group)
user.group = group_select
if admin_groups and role == 'GA': # 如果是组管理员就要添加组管理员和组到管理组中
for group_id in admin_groups:
group = get_object(UserGroup, id=group_id)
if group:
AdminGroup(user=user, group=group).save()
return user
def db_update_user(**kwargs):
"""
update a user info in database
数据库更新用户信息
"""
groups_post = kwargs.pop('groups')
admin_groups_post = kwargs.pop('admin_groups')
user_id = kwargs.pop('user_id')
user = User.objects.filter(id=user_id)
if user:
user_get = user[0]
password = kwargs.pop('password')
user.update(**kwargs)
if password.strip():
user_get.set_password(password)
user_get.save()
else:
return None
group_select = []
if groups_post:
for group_id in groups_post:
group = UserGroup.objects.filter(id=group_id)
group_select.extend(group)
user_get.group = group_select
if admin_groups_post != '':
user_get.admingroup_set.all().delete()
for group_id in admin_groups_post:
group = get_object(UserGroup, id=group_id)
AdminGroup(user=user, group=group).save()
def db_del_user(username):
"""
delete a user from database
从数据库中删除用户
"""
user = get_object(User, username=username)
if user:
user.delete()
def gen_ssh_key(username, password='',
key_dir=os.path.join(KEY_DIR, 'user'),
authorized_keys=True, home="/home", length=2048):
"""
generate a user ssh key in a property dir
生成一个用户ssh密钥对
"""
logger.debug('生成ssh key 并设置authorized_keys')
private_key_file = os.path.join(key_dir, username+'.pem')
mkdir(key_dir, mode=0777)
if os.path.isfile(private_key_file):
os.unlink(private_key_file)
ret = bash('echo -e "y\n"|ssh-keygen -t rsa -f %s -b %s -P "%s"' % (private_key_file, length, password))
if authorized_keys:
auth_key_dir = os.path.join(home, username, '.ssh')
mkdir(auth_key_dir, username=username, mode=0700)
authorized_key_file = os.path.join(auth_key_dir, 'authorized_keys')
with open(private_key_file+'.pub') as pub_f:
with open(authorized_key_file, 'w') as auth_f:
auth_f.write(pub_f.read())
os.chmod(authorized_key_file, 0600)
chown(authorized_key_file, username)
def server_add_user(username, ssh_key_pwd=''):
"""
add a system user in jumpserver
在jumpserver服务器上添加一个用户
"""
bash("useradd -s '%s' '%s'" % (os.path.join(BASE_DIR, 'init.sh'), username))
gen_ssh_key(username, ssh_key_pwd)
def user_add_mail(user, kwargs):
"""
add user send mail
发送用户添加邮件
"""
user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'}
mail_title = u'恭喜你的跳板机用户 %s 添加成功 Jumpserver' % user.name
mail_msg = u"""
Hi, %s
您的用户名: %s
您的权限: %s
您的web登录密码 %s
您的ssh密钥文件密码 %s
密钥下载地址: %s/juser/key/down/?uuid=%s
说明: 请登陆跳板机后台下载密钥, 然后使用密钥登陆跳板机!
""" % (user.name, user.username, user_role.get(user.role, u'普通用户'),
kwargs.get('password'), kwargs.get('ssh_key_pwd'), URL, user.uuid)
send_mail(mail_title, mail_msg, MAIL_FROM, [user.email], fail_silently=False)
def server_del_user(username):
"""
delete a user from jumpserver linux system
删除系统上的某用户
"""
bash('userdel -r -f %s' % username)
def get_display_msg(user, password='', ssh_key_pwd='', send_mail_need=False):
if send_mail_need:
msg = u'添加用户 %s 成功! 用户密码已发送到 %s 邮箱!' % (user.name, user.email)
else:
msg = u"""
跳板机地址: %s <br />
用户名:%s <br />
密码:%s <br />
密钥密码:%s <br />
密钥下载url: %s/juser/key/down/?uuid=%s <br />
该账号密码可以登陆web和跳板机。
""" % (URL, user.username, password, ssh_key_pwd, URL, user.uuid)
return msg

466
juser/views.py Normal file
View File

@@ -0,0 +1,466 @@
# coding: utf-8
# Author: Guanghongwei
# Email: ibuler@qq.com
# import random
# from Crypto.PublicKey import RSA
import uuid
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
from django.db.models import Q
from juser.user_api import *
from jperm.perm_api import get_group_user_perm
MAIL_FROM = EMAIL_HOST_USER
@require_role(role='super')
def group_add(request):
"""
group add view for route
添加用户组的视图
"""
error = ''
msg = ''
header_title, path1, path2 = '添加用户组', '用户管理', '添加用户组'
user_all = User.objects.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 group_name:
error = u'组名 不能为空'
raise ServerError(error)
if UserGroup.objects.filter(name=group_name):
error = u'组名已存在'
raise ServerError(error)
db_add_group(name=group_name, users_id=users_selected, comment=comment)
except ServerError:
pass
except TypeError:
error = u'添加小组失败'
else:
msg = u'添加组 %s 成功' % group_name
return my_render('juser/group_add.html', locals(), request)
@require_role(role='super')
def group_list(request):
"""
list user group
用户组列表
"""
header_title, path1, path2 = '查看用户组', '用户管理', '查看用户组'
keyword = request.GET.get('search', '')
user_group_list = UserGroup.objects.all().order_by('name')
group_id = request.GET.get('id', '')
if keyword:
user_group_list = user_group_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword))
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_role(role='super')
def group_del(request):
"""
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('删除成功')
@require_role(role='super')
def group_edit(request):
error = ''
msg = ''
header_title, path1, path2 = '编辑用户组', '用户管理', '编辑用户组'
if request.method == 'GET':
group_id = request.GET.get('id', '')
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()
elif request.method == 'POST':
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')
try:
if '' in [group_id, group_name]:
raise ServerError('组名不能为空')
if len(UserGroup.objects.filter(name=group_name)) > 1:
raise ServerError(u'%s 用户组已存在' % group_name)
# add user group
user_group = get_object_or_404(UserGroup, id=group_id)
user_group.user_set.clear()
for user in User.objects.filter(id__in=users_selected):
user.group.add(UserGroup.objects.get(id=group_id))
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 my_render('juser/group_edit.html', locals(), request)
@require_role(role='super')
def user_add(request):
error = ''
msg = ''
header_title, path1, path2 = '添加用户', '用户管理', '添加用户'
user_role = {'SU': u'超级管理员', 'CU': u'普通用户'}
group_all = UserGroup.objects.all()
if request.method == 'POST':
username = request.POST.get('username', '')
password = PyCrypt.gen_rand_pass(16)
name = request.POST.get('name', '')
email = request.POST.get('email', '')
groups = request.POST.getlist('groups', [])
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
send_mail_need = True if '1' in extra else False
try:
if '' in [username, password, ssh_key_pwd, name, role]:
error = u'带*内容不能为空'
raise ServerError
check_user_is_exist = User.objects.filter(username=username)
if check_user_is_exist:
error = u'用户 %s 已存在' % username
raise ServerError
except ServerError:
pass
else:
try:
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=username, ssh_key_pwd=ssh_key_pwd)
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))
except IndexError, e:
error = u'添加用户 %s 失败 %s ' % (username, e)
try:
db_del_user(username)
server_del_user(username)
except Exception:
pass
else:
if MAIL_ENABLE and send_mail_need:
user_add_mail(user, kwargs=locals())
msg = get_display_msg(user, password=password, ssh_key_pwd=ssh_key_pwd, send_mail_need=send_mail_need)
return my_render('juser/user_add.html', locals(), request)
@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', '')
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]
users_list = user_group.user_set.all()
if keyword:
users_list = users_list.filter(Q(username__icontains=keyword) | Q(name__icontains=keyword)).order_by('username')
users_list, p, users, page_range, current_page, show_first, show_end = pages(users_list, request)
return my_render('juser/user_list.html', locals(), request)
@require_role(role='user')
def user_detail(request):
header_title, path1, path2 = '用户详情', '用户管理', '用户详情'
if request.session.get('role_id') == 0:
user_id = request.user.id
else:
user_id = request.GET.get('id', '')
user = get_object(User, id=user_id)
if not user:
return HttpResponseRedirect(reverse('user_list'))
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_role(role='admin')
def user_del(request):
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('错误请求')
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()
return HttpResponse('删除成功')
@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&timestamp=%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&timestamp=%s&hash=%s' % (uuid_r, timestamp, hash_encode)
if hash_encode == PyCrypt.md5_crypt(uuid_r + timestamp + KEY):
if int(time.time()) - int(timestamp) > 600:
return http_error(request, u'链接已超时')
else:
return HttpResponse('hash校验失败')
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('用户不存在')
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 = '编辑用户', '用户管理', '编辑用户'
if request.method == 'GET':
user_id = request.GET.get('id', '')
if not user_id:
return HttpResponseRedirect(reverse('index'))
user_role = {'SU': u'超级管理员', 'CU': u'普通用户'}
user = get_object(User, id=user_id)
group_all = UserGroup.objects.all()
if user:
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.GET.get('id', '')
password = request.POST.get('password', '')
name = request.POST.get('name', '')
email = request.POST.get('email', '')
groups = request.POST.getlist('groups', [])
role_post = request.POST.get('role', 'CU')
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 '1' in extra else False
user_role = {'SU': u'超级管理员', 'GA': u'部门管理员', 'CU': u'普通用户'}
if user_id:
user = get_object(User, id=user_id)
else:
return HttpResponseRedirect(reverse('user_list'))
db_update_user(user_id=user_id,
password=password,
name=name,
email=email,
groups=groups,
admin_groups=admin_groups,
role=role_post,
is_active=is_active)
if email_need:
msg = u"""
Hi %s:
您的信息已修改,请登录跳板机查看详细信息
地址:%s
用户名: %s
密码:%s (如果密码为None代表密码为原密码)
权限::%s
""" % (user.name, URL, user.username, password, 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.user.id
if not user_id:
return HttpResponseRedirect(reverse('index'))
user = User.objects.get(id=user_id)
return my_render('juser/profile.html', locals(), request)
def change_info(request):
header_title, path1, path2 = '修改信息', '用户管理', '修改个人信息'
user_id = request.user.id
user = User.objects.get(id=user_id)
error = ''
if not user:
return HttpResponseRedirect(reverse('index'))
if request.method == 'POST':
name = request.POST.get('name', '')
password = request.POST.get('password', '')
email = request.POST.get('email', '')
if '' in [name, email]:
error = '不能为空'
if not error:
User.objects.filter(id=user_id).update(name=name, email=email)
if len(password) > 0:
user.set_password(password)
user.save()
msg = '修改成功'
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_role(role='user')
def down_key(request):
if is_role_request(request, 'super'):
uuid_r = request.GET.get('uuid', '')
else:
uuid_r = request.user.uuid
if uuid_r:
user = get_object(User, uuid=uuid_r)
if user:
username = user.username
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()
f.close()
response = HttpResponse(data, content_type='application/octet-stream')
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(private_key_file)
if request.user.role == 'CU':
os.unlink(private_key_file)
return response
return HttpResponse('No Key File. Contact Admin.')

3
keys/README.md Normal file
View File

@@ -0,0 +1,3 @@
看山是山,看水是水
看山不是山,看水不是水
看山是山,看水是水

View File

@@ -1,30 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,D5AF0CC627FA8B85
oQLAgKLelCYNGc7cP51Bx3b66GmCKHAEhqBCsrPEi4eYKV7D30WOp+sdp8F5RaK1
I/ZtGmv4jAaK4/KCBr2bGgH7cnkOhdj6T+ro9YVr92r5kD0gwHzYzbTCZP44N9Wn
83pUy4eUpMB659Y4TQgUAQVejyr1/ZZJSb8Kgah1xlLlM1ZSmK7PuqCSpBz16wil
TXrywWvsUhrAuAJh1NC7wr7mXm8tb2DMkT1+ckIQUqhOMJGEvgByI3SXUKR1c9lL
ZBh7RwukipvbujazxJxBvW50fLpdSDuBIxRsQhJ5PvYTV8BBwsp5KvbO5b/qkIOL
RUxY4q3o7fHtXiRf4vD9iD1U7F39UMnDIie6jk642oN4aYmvyc0SnVH8RcVarnCA
HkdZ/5c93BCZRovay6DoIumN/OmURe83A9hzsHnMg9hkLAapw5dmlsvtk307YWK8
YnAy7TMLS72RdAxSJHXN9U+U7QKDu47jiIkST3kwNLXYm4gvY7eFlg82Ux2h9Soh
R5qJ6pdYh1Ah/msbE9v/HkWCqhU6fD38MaAmcuCAoOh2LuI+32+2ZkCT04hPhzQL
AjfrMsUFKiH/+IrFtQ8CcwaFtVR9b/Ebe65SQuBXV3Bcrcx91MWKBjCpiB1rQQZ/
mxNh4jQmvUOJYhDv8D1TZMCLrAREt07uPLhL24klv6v4lf204xbLhdYA1kW/m/16
+oEH6CHMo8qVqWylATfya1Iv+htAXs6Pi65j6QQjyM/sQZ+Cn0enGSS5S9WIx+Ca
C+kbRBmxfwYXROu7NkmWexiU3a7VmTj/0yA8uRXY7jbEyGgupXiAeXa2/pRX2XRo
8MugyIoZzLxvkszQ90WxH/9hO+eNDY1Wo+ONo8deos3pogjeIPhiFQlx2EFowGcf
TcNFgluQliwVm5qEWe2SWoNlHgnI7rhus1yAexH/G2uJHpd8eid3Rn19Fh1TxD16
r/+I6TEp7u27Fc9T+HVXg46kXxf7+p0/fcHLxOk0A5wfLzQgB6fVvXoXoldWoomr
FXpKmDTDjTp15R1pVZ3eVtuJp3wA+N6YsL7F2NsmtKMaMrXuMwSwJUJPpFSD/DXe
BUcqSeQMwvwSgzH4qkBtJcQ6baEVgBvDeRKoVInZ9D7FpumTv5tSnsqyD0NBZsXC
qmA5Sk+w2CRW1Nbi/uSFdDACZo8iDhILJ9fHRtNLyB+BN2cN7dIQZlmOQxK3omqd
iCgu1q31iE8wgxzEnrtPxLWi/2jr7WX0NVFzE6Qjjpvqa+3k4MjNDeNfPKHJMK50
o8hf4jlSZxhjc4D+U9ksSJZOl6NyuLhpp8YlVSkZ/Oiz6U1reTf8qZUOkBgY5V2T
0N+Pwqs81nA4Hsh32MxuVIt0RLtp2uLUyeEHatEp3akABFvWWMWbxgbwsmOXtnsj
QxJQ6jL+CCvpufu34prkMeKPApqTrwHq9YLNBFgg4ts+pl8wHrFo1CFma2ljvFzq
8FNn8FDJcebasNmnJOvXTryqp/Qd85fKH0XuR100azmXnuRmxuDmNXfN/ejtne1b
ZyDrSEGOlr/GpxTLDtbSSu82I+XqvAicXsUqbOqWUPM2mAKtDVfaIQ==
-----END RSA PRIVATE KEY-----

1
logs/README.md Normal file
View File

@@ -0,0 +1 @@
永远年轻,永远热泪盈眶

View File

@@ -3,7 +3,7 @@ import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "AutoSa.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
from django.core.management import execute_from_command_line

519
run_server.py Executable file
View File

@@ -0,0 +1,519 @@
#!/usr/bin/env python
# coding: utf-8
import time
import datetime
import json
import os
import sys
import os.path
import threading
import re
import functools
from django.core.signals import request_started, request_finished
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
import tornado.httpserver
import tornado.gen
import tornado.httpclient
from tornado.websocket import WebSocketClosedError
from tornado.options import define, options
from pyinotify import WatchManager, ProcessEvent, IN_DELETE, IN_CREATE, IN_MODIFY, AsyncNotifier
import select
from connect import Tty, User, Asset, PermRole, logger, get_object, gen_resource
from connect import TtyLog, Log, Session, user_have_perm, get_group_user_perm, MyRunner, ExecLog
try:
import simplejson as json
except ImportError:
import json
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
from jumpserver.settings import IP, PORT
define("port", default=PORT, help="run on the given port", type=int)
define("host", default=IP, help="run port on given host", type=str)
from jlog.views import TermLogRecorder
def django_request_support(func):
@functools.wraps(func)
def _deco(*args, **kwargs):
request_started.send_robust(func)
response = func(*args, **kwargs)
request_finished.send_robust(func)
return response
return _deco
def require_auth(role='user'):
def _deco(func):
def _deco2(request, *args, **kwargs):
if request.get_cookie('sessionid'):
session_key = request.get_cookie('sessionid')
else:
session_key = request.get_argument('sessionid', '')
logger.debug('Websocket: session_key: %s' % session_key)
if session_key:
session = get_object(Session, session_key=session_key)
logger.debug('Websocket: session: %s' % session)
if session and datetime.datetime.now() < session.expire_date:
user_id = session.get_decoded().get('_auth_user_id')
request.user_id = user_id
user = get_object(User, id=user_id)
if user:
logger.debug('Websocket: user [ %s ] request websocket' % user.username)
request.user = user
if role == 'admin':
if user.role in ['SU', 'GA']:
return func(request, *args, **kwargs)
logger.debug('Websocket: user [ %s ] is not admin.' % user.username)
else:
return func(request, *args, **kwargs)
else:
logger.debug('Websocket: session expired: %s' % session_key)
try:
request.close()
except AttributeError:
pass
logger.warning('Websocket: Request auth failed.')
return _deco2
return _deco
class MyThread(threading.Thread):
def __init__(self, *args, **kwargs):
super(MyThread, self).__init__(*args, **kwargs)
def run(self):
try:
super(MyThread, self).run()
except WebSocketClosedError:
pass
class EventHandler(ProcessEvent):
def __init__(self, client=None):
self.client = client
def process_IN_MODIFY(self, event):
self.client.write_message(f.read().decode('utf-8', 'replace'))
def file_monitor(path='.', client=None):
wm = WatchManager()
mask = IN_DELETE | IN_CREATE | IN_MODIFY
notifier = AsyncNotifier(wm, EventHandler(client))
wm.add_watch(path, mask, auto_add=True, rec=True)
if not os.path.isfile(path):
logger.debug("File %s does not exist." % path)
sys.exit(3)
else:
logger.debug("Now starting monitor file %s." % path)
global f
f = open(path, 'r')
st_size = os.stat(path)[6]
f.seek(st_size)
while True:
try:
notifier.process_events()
if notifier.check_events():
notifier.read_events()
except KeyboardInterrupt:
print "keyboard Interrupt."
notifier.stop()
break
class MonitorHandler(tornado.websocket.WebSocketHandler):
clients = []
threads = []
def __init__(self, *args, **kwargs):
self.file_path = None
super(self.__class__, self).__init__(*args, **kwargs)
def check_origin(self, origin):
return True
@django_request_support
@require_auth('admin')
def open(self):
# 获取监控的path
self.file_path = self.get_argument('file_path', '')
MonitorHandler.clients.append(self)
thread = MyThread(target=file_monitor, args=('%s.log' % self.file_path, self))
MonitorHandler.threads.append(thread)
self.stream.set_nodelay(True)
try:
for t in MonitorHandler.threads:
if t.is_alive():
continue
t.setDaemon(True)
t.start()
except WebSocketClosedError:
client_index = MonitorHandler.clients.index(self)
MonitorHandler.threads[client_index].stop()
MonitorHandler.clients.remove(self)
MonitorHandler.threads.remove(MonitorHandler.threads[client_index])
logger.debug("Websocket: Monitor client num: %s, thread num: %s" % (len(MonitorHandler.clients),
len(MonitorHandler.threads)))
def on_message(self, message):
# 监控日志,发生变动发向客户端
pass
def on_close(self):
# 客户端主动关闭
# self.close()
logger.debug("Websocket: Monitor client close request")
try:
client_index = MonitorHandler.clients.index(self)
MonitorHandler.clients.remove(self)
MonitorHandler.threads.remove(MonitorHandler.threads[client_index])
except ValueError:
pass
class WebTty(Tty):
def __init__(self, *args, **kwargs):
super(WebTty, self).__init__(*args, **kwargs)
self.ws = None
self.data = ''
self.input_mode = False
class WebTerminalKillHandler(tornado.web.RequestHandler):
@django_request_support
@require_auth('admin')
def get(self):
ws_id = self.get_argument('id')
Log.objects.filter(id=ws_id).update(is_finished=True)
for ws in WebTerminalHandler.clients:
if ws.id == int(ws_id):
logger.debug("Kill log id %s" % ws_id)
ws.log.save()
ws.close()
logger.debug('Websocket: web terminal client num: %s' % len(WebTerminalHandler.clients))
class ExecHandler(tornado.websocket.WebSocketHandler):
clients = []
tasks = []
def __init__(self, *args, **kwargs):
self.id = 0
self.user = None
self.role = None
self.runner = None
self.assets = []
self.perm = {}
self.remote_ip = ''
super(ExecHandler, self).__init__(*args, **kwargs)
def check_origin(self, origin):
return True
@django_request_support
@require_auth('user')
def open(self):
logger.debug('Websocket: Open exec request')
role_name = self.get_argument('role', 'sb')
self.remote_ip = self.request.remote_ip
logger.debug('Web执行命令: 请求系统用户 %s' % role_name)
self.role = get_object(PermRole, name=role_name)
self.perm = get_group_user_perm(self.user)
roles = self.perm.get('role').keys()
if self.role not in roles:
self.write_message('No perm that role %s' % role_name)
self.close()
self.assets = self.perm.get('role').get(self.role).get('asset')
res = gen_resource({'user': self.user, 'asset': self.assets, 'role': self.role})
self.runner = MyRunner(res)
message = '有权限的主机: ' + ', '.join([asset.hostname for asset in self.assets])
self.__class__.clients.append(self)
self.write_message(message)
def on_message(self, message):
data = json.loads(message)
pattern = data.get('pattern', '')
self.command = data.get('command', '')
self.asset_name_str = ''
if pattern and self.command:
for inv in self.runner.inventory.get_hosts(pattern=pattern):
self.asset_name_str += '%s ' % inv.name
self.write_message('匹配主机: ' + self.asset_name_str)
self.write_message('<span style="color: yellow">Ansible> %s</span>\n\n' % self.command)
self.__class__.tasks.append(MyThread(target=self.run_cmd, args=(self.command, pattern)))
for t in self.__class__.tasks:
if t.is_alive():
continue
try:
t.setDaemon(True)
t.start()
except RuntimeError:
pass
def run_cmd(self, command, pattern):
self.runner.run('shell', command, pattern=pattern)
ExecLog(host=self.asset_name_str, cmd=self.command, user=self.user.username,
remote_ip=self.remote_ip, result=self.runner.results).save()
newline_pattern = re.compile(r'\n')
for k, v in self.runner.results.items():
for host, output in v.items():
output = newline_pattern.sub('<br />', output)
if k == 'ok':
header = "<span style='color: green'>[ %s => %s]</span>\n" % (host, 'Ok')
else:
header = "<span style='color: red'>[ %s => %s]</span>\n" % (host, 'failed')
self.write_message(header)
self.write_message(output)
self.write_message('\n~o~ Task finished ~o~\n')
def on_close(self):
logger.debug('关闭web_exec请求')
class WebTerminalHandler(tornado.websocket.WebSocketHandler):
clients = []
tasks = []
def __init__(self, *args, **kwargs):
self.term = None
self.log_file_f = None
self.log_time_f = None
self.log = None
self.id = 0
self.user = None
self.ssh = None
self.channel = None
super(WebTerminalHandler, self).__init__(*args, **kwargs)
def check_origin(self, origin):
return True
@django_request_support
@require_auth('user')
def open(self):
logger.debug('Websocket: Open request')
role_name = self.get_argument('role', 'sb')
asset_id = self.get_argument('id', 9999)
asset = get_object(Asset, id=asset_id)
self.termlog = TermLogRecorder(User.objects.get(id=self.user_id))
if asset:
roles = user_have_perm(self.user, asset)
logger.debug(roles)
logger.debug('系统用户: %s' % role_name)
login_role = ''
for role in roles:
if role.name == role_name:
login_role = role
break
if not login_role:
logger.warning('Websocket: Not that Role %s for Host: %s User: %s ' % (role_name, asset.hostname,
self.user.username))
self.close()
return
else:
logger.warning('Websocket: No that Host: %s User: %s ' % (asset_id, self.user.username))
self.close()
return
logger.debug('Websocket: request web terminal Host: %s User: %s Role: %s' % (asset.hostname, self.user.username,
login_role.name))
self.term = WebTty(self.user, asset, login_role, login_type='web')
# self.term.remote_ip = self.request.remote_ip
self.term.remote_ip = self.request.headers.get("X-Real-IP")
if not self.term.remote_ip:
self.term.remote_ip = self.request.remote_ip
self.ssh = self.term.get_connection()
self.channel = self.ssh.invoke_shell(term='xterm')
WebTerminalHandler.tasks.append(MyThread(target=self.forward_outbound))
WebTerminalHandler.clients.append(self)
for t in WebTerminalHandler.tasks:
if t.is_alive():
continue
try:
t.setDaemon(True)
t.start()
except RuntimeError:
pass
def on_message(self, message):
jsondata = json.loads(message)
if not jsondata:
return
if 'resize' in jsondata.get('data'):
self.channel.resize_pty(
jsondata.get('data').get('resize').get('cols', 80),
jsondata.get('data').get('resize').get('rows', 24)
)
elif jsondata.get('data'):
self.termlog.recoder = True
self.term.input_mode = True
if str(jsondata['data']) in ['\r', '\n', '\r\n']:
if self.term.vim_flag:
match = re.compile(r'\x1b\[\?1049', re.X).findall(self.term.vim_data)
if match:
if self.term.vim_end_flag or len(match) == 2:
self.term.vim_flag = False
self.term.vim_end_flag = False
else:
self.term.vim_end_flag = True
else:
result = self.term.deal_command(self.term.data)[0:200]
if len(result) > 0:
TtyLog(log=self.log, datetime=datetime.datetime.now(), cmd=result).save()
self.term.vim_data = ''
self.term.data = ''
self.term.input_mode = False
self.channel.send(jsondata['data'])
else:
pass
def on_close(self):
logger.debug('Websocket: Close request')
print self.termlog.CMD
self.termlog.save()
if self in WebTerminalHandler.clients:
WebTerminalHandler.clients.remove(self)
try:
self.log_file_f.write('End time is %s' % datetime.datetime.now())
self.log.is_finished = True
self.log.end_time = datetime.datetime.now()
self.log.filename = self.termlog.filename
self.log.save()
self.log_time_f.close()
self.ssh.close()
self.close()
except AttributeError:
pass
def forward_outbound(self):
self.log_file_f, self.log_time_f, self.log = self.term.get_log()
self.id = self.log.id
self.termlog.setid(self.id)
try:
data = ''
pre_timestamp = time.time()
while True:
r, w, e = select.select([self.channel, sys.stdin], [], [])
if self.channel in r:
recv = self.channel.recv(1024)
if not len(recv):
return
data += recv
if self.term.vim_flag:
self.term.vim_data += recv
try:
self.write_message(data.decode('utf-8', 'replace'))
self.termlog.write(data)
self.termlog.recoder = False
now_timestamp = time.time()
self.log_time_f.write('%s %s\n' % (round(now_timestamp - pre_timestamp, 4), len(data)))
self.log_file_f.write(data)
pre_timestamp = now_timestamp
self.log_file_f.flush()
self.log_time_f.flush()
if self.term.input_mode and not self.term.is_output(data):
self.term.data += data
data = ''
except UnicodeDecodeError:
pass
except IndexError:
pass
# class MonitorHandler(WebTerminalHandler):
# @django_request_support
# @require_auth('user')
# def open(self):
# try:
# self.returnlog = TermLogRecorder.loglist[self.get_argument('id')]
# self.returnlog.write_message = self.write_message
# except:
# self.write_message('Log is None')
# self.close()
#
# def on_message(self, message):
# pass
#
# def on_close(self):
# self.close()
class Application(tornado.web.Application):
def __init__(self):
handlers = [
(r'/monitor', MonitorHandler),
(r'/terminal', WebTerminalHandler),
(r'/kill', WebTerminalKillHandler),
(r'/exec', ExecHandler),
]
setting = {
'cookie_secret': 'DFksdfsasdfkasdfFKwlwfsdfsa1204mx',
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
'debug': False,
}
tornado.web.Application.__init__(self, handlers, **setting)
def main():
from django.core.wsgi import get_wsgi_application
import tornado.wsgi
wsgi_app = get_wsgi_application()
container = tornado.wsgi.WSGIContainer(wsgi_app)
setting = {
'cookie_secret': 'DFksdfsasdfkasdfFKwlwfsdfsa1204mx',
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
'debug': False,
}
tornado_app = tornado.web.Application(
[
(r'/ws/monitor', MonitorHandler),
(r'/ws/terminal', WebTerminalHandler),
(r'/kill', WebTerminalKillHandler),
(r'/ws/exec', ExecHandler),
(r"/static/(.*)", tornado.web.StaticFileHandler,
dict(path=os.path.join(os.path.dirname(__file__), "static"))),
('.*', tornado.web.FallbackHandler, dict(fallback=container)),
], **setting)
server = tornado.httpserver.HTTPServer(tornado_app)
server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
if __name__ == '__main__':
# tornado.options.parse_command_line()
# app = Application()
# server = tornado.httpserver.HTTPServer(app)
# server.bind(options.port, options.host)
# #server.listen(options.port)
# server.start(num_processes=5)
# tornado.ioloop.IOLoop.instance().start()
print "Run server on %s:%s" % (options.host, options.port)
main()

View File

@@ -1,18 +0,0 @@
#!/bin/bash
manage_file="./webroot/AutoSa/manage.py"
log_handler_file="./webroot/AutoSa/log_handler.py"
websocket_file="./webroot/AutoSa/websocket/index.js"
which node &> /dev/null
if [ $? != '0' ];then
echo "Please define the node.js binary file 'node' in the PATH."
exit
fi
node $websocket_file &
if [ -f $manage_file -a -e $manage_file ] && [ -f $log_handler_file -a -e $log_handler_file ];then
$manage_file runserver 0.0.0.0:80 &> logs/access.log &
$log_handler_file &> logs/handler.log &
else
echo "manage.py or log_handler.py isn't exist or executable."
fi

View File

@@ -1,13 +0,0 @@
#!/bin/bash
if [ $USER == 'admin' ] || [ $USER == 'root' ];then
echo ""
else
python /opt/jumpserver/jumpserver.py
if [ $USER == 'guanghongwei' ];then
echo
else
exit 3
echo
fi
fi

View File

@@ -1,8 +0,0 @@
pexpect==3.3
sphinx-me==0.3
django==1.7.1
python-ldap==2.4.18
paramiko==1.15.1
pycrypto==2.6.1
ecdsa>=0.11
MySQL-python==1.2.5

115
service.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/bin/bash
# jumpserver Startup script for the jumpserver Server
#
# chkconfig: - 85 12
# description: Open source detecting system
# processname: jumpserver
# Date: 2016-02-27
# Version: 3.0.1
# Site: http://www.jumpserver.org
# Author: Jumpserver Team
jumpserver_dir=
base_dir=$(dirname $0)
jumpserver_dir=${jumpserver_dir:-$base_dir}
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
if [ -f ${jumpserver_dir}/install/functions ];then
. ${jumpserver_dir}/install/functions
elif [ -f /etc/init.d/functions ];then
. /etc/init.d/functions
else
echo "No functions script found in [./functions, ./install/functions, /etc/init.d/functions]"
exit 1
fi
PROC_NAME="jumpserver"
lockfile=/var/lock/subsys/${PROC_NAME}
start() {
jump_start=$"Starting ${PROC_NAME} service:"
if [ -f $lockfile ];then
echo -n "jumpserver is running..."
success "$jump_start"
echo
else
daemon python $jumpserver_dir/manage.py crontab add &>> /var/log/jumpserver.log 2>&1
daemon python $jumpserver_dir/run_server.py &> /dev/null 2>&1 &
sleep 1
echo -n "$jump_start"
ps axu | grep 'run_server' | grep -v 'grep' &> /dev/null
if [ $? == '0' ];then
success "$jump_start"
if [ ! -e $lockfile ]; then
lockfile_dir=`dirname $lockfile`
mkdir -pv $lockfile_dir
fi
touch "$lockfile"
echo
else
failure "$jump_start"
echo
fi
fi
}
stop() {
echo -n $"Stopping ${PROC_NAME} service:"
daemon python $jumpserver_dir/manage.py crontab remove &>> /var/log/jumpserver.log 2>&1
ps aux | grep -E 'run_server.py' | grep -v grep | awk '{print $2}' | xargs kill -9 &> /dev/null
ret=$?
if [ $ret -eq 0 ]; then
echo_success
echo
rm -f "$lockfile"
else
echo_failure
echo
rm -f "$lockfile"
fi
}
status(){
ps axu | grep 'run_server' | grep -v 'grep' &> /dev/null
if [ $? == '0' ];then
echo -n "jumpserver is running..."
success
touch "$lockfile"
echo
else
echo -n "jumpserver is not running."
failure
echo
fi
}
restart(){
stop
start
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo $"Usage: $0 {start|stop|restart|status}"
exit 2
esac

2848
static/css/animate.css vendored Normal file

File diff suppressed because it is too large Load Diff

5
static/css/bootstrap.min.css vendored Normal file

File diff suppressed because one or more lines are too long

50
static/css/colorbox.css Normal file
View File

@@ -0,0 +1,50 @@
/*
Colorbox Core Style:
The following CSS is consistent between example themes and should not be altered.
*/
#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
#cboxWrapper {max-width:none;}
#cboxOverlay{position:fixed; width:100%; height:100%;}
#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
#cboxContent{position:relative;}
#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;}
#cboxTitle{margin:0;}
#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none; -ms-interpolation-mode:bicubic;}
.cboxIframe{width:100%; height:100%; display:block; border:0; padding:0; margin:0;}
#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;}
/*
User Style:
Change the following styles to modify the appearance of Colorbox. They are
ordered & tabbed in a way that represents the nesting of the generated HTML.
*/
#cboxOverlay{background:#fff; opacity: 0.9; filter: alpha(opacity = 90);}
#colorbox{outline:0;}
#cboxContent{margin-top:32px; overflow:visible; background:#000;}
.cboxIframe{background:#fff;}
#cboxError{padding:50px; border:1px solid #ccc;}
#cboxLoadedContent{background:#000; padding:1px;}
#cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;}
#cboxLoadingOverlay{background:#000;}
#cboxTitle{position:absolute; top:-22px; left:0; color:#000;}
#cboxCurrent{position:absolute; top:-22px; right:205px; text-indent:-9999px;}
/* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */
#cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; text-indent:-9999px; width:20px; height:20px; position:absolute; top:-20px; background:url(images/controls.png) no-repeat 0 0;}
/* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */
#cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;}
#cboxPrevious{background-position:0px 0px; right:44px;}
#cboxPrevious:hover{background-position:0px -25px;}
#cboxNext{background-position:-25px 0px; right:22px;}
#cboxNext:hover{background-position:-25px -25px;}
#cboxClose{background-position:-50px 0px; right:0;}
#cboxClose:hover{background-position:-50px -25px;}
.cboxSlideshow_on #cboxPrevious, .cboxSlideshow_off #cboxPrevious{right:66px;}
.cboxSlideshow_on #cboxSlideshow{background-position:-75px -25px; right:44px;}
.cboxSlideshow_on #cboxSlideshow:hover{background-position:-100px -25px;}
.cboxSlideshow_off #cboxSlideshow{background-position:-100px 0px; right:44px;}
.cboxSlideshow_off #cboxSlideshow:hover{background-position:-75px -25px;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,374 @@
/* Magnific Popup CSS */
.mfp-bg {
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1042;
overflow: hidden;
position: fixed;
background: #0b0b0b;
opacity: 0.8;
filter: alpha(opacity=80); }
.mfp-wrap {
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1043;
position: fixed;
outline: none !important;
-webkit-backface-visibility: hidden; }
.mfp-container {
text-align: center;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
padding: 0 8px;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
.mfp-container:before {
content: '';
display: inline-block;
height: 100%;
vertical-align: middle; }
.mfp-align-top .mfp-container:before {
display: none; }
.mfp-content {
position: relative;
display: inline-block;
vertical-align: middle;
margin: 0 auto;
text-align: left;
z-index: 1045; }
.mfp-inline-holder .mfp-content, .mfp-ajax-holder .mfp-content {
width: 100%;
cursor: auto; }
.mfp-ajax-cur {
cursor: progress; }
.mfp-zoom-out-cur, .mfp-zoom-out-cur .mfp-image-holder .mfp-close {
cursor: -moz-zoom-out;
cursor: -webkit-zoom-out;
cursor: zoom-out; }
.mfp-zoom {
cursor: pointer;
cursor: -webkit-zoom-in;
cursor: -moz-zoom-in;
cursor: zoom-in; }
.mfp-auto-cursor .mfp-content {
cursor: auto; }
.mfp-close, .mfp-arrow, .mfp-preloader, .mfp-counter {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none; }
.mfp-loading.mfp-figure {
display: none; }
.mfp-hide {
display: none !important; }
.mfp-preloader {
color: #CCC;
position: absolute;
top: 50%;
width: auto;
text-align: center;
margin-top: -0.8em;
left: 8px;
right: 8px;
z-index: 1044; }
.mfp-preloader a {
color: #CCC; }
.mfp-preloader a:hover {
color: #FFF; }
.mfp-s-ready .mfp-preloader {
display: none; }
.mfp-s-error .mfp-content {
display: none; }
button.mfp-close, button.mfp-arrow {
overflow: visible;
cursor: pointer;
background: transparent;
border: 0;
-webkit-appearance: none;
display: block;
outline: none;
padding: 0;
z-index: 1046;
-webkit-box-shadow: none;
box-shadow: none; }
button::-moz-focus-inner {
padding: 0;
border: 0; }
.mfp-close {
width: 44px;
height: 44px;
line-height: 44px;
position: absolute;
right: 0;
top: 0;
text-decoration: none;
text-align: center;
opacity: 0.65;
filter: alpha(opacity=65);
padding: 0 0 18px 10px;
color: #FFF;
font-style: normal;
font-size: 28px;
font-family: Arial, Baskerville, monospace; }
.mfp-close:hover, .mfp-close:focus {
opacity: 1;
filter: alpha(opacity=100); }
.mfp-close:active {
top: 1px; }
.mfp-close-btn-in .mfp-close {
color: #333; }
.mfp-image-holder .mfp-close, .mfp-iframe-holder .mfp-close {
color: #FFF;
right: -6px;
text-align: right;
padding-right: 6px;
width: 100%; }
.mfp-counter {
position: absolute;
top: 0;
right: 0;
color: #CCC;
font-size: 12px;
line-height: 18px;
white-space: nowrap; }
.mfp-arrow {
position: absolute;
opacity: 0.65;
filter: alpha(opacity=65);
margin: 0;
top: 50%;
margin-top: -55px;
padding: 0;
width: 90px;
height: 110px;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
.mfp-arrow:active {
margin-top: -54px; }
.mfp-arrow:hover, .mfp-arrow:focus {
opacity: 1;
filter: alpha(opacity=100); }
.mfp-arrow:before, .mfp-arrow:after, .mfp-arrow .mfp-b, .mfp-arrow .mfp-a {
content: '';
display: block;
width: 0;
height: 0;
position: absolute;
left: 0;
top: 0;
margin-top: 35px;
margin-left: 35px;
border: medium inset transparent; }
.mfp-arrow:after, .mfp-arrow .mfp-a {
border-top-width: 13px;
border-bottom-width: 13px;
top: 8px; }
.mfp-arrow:before, .mfp-arrow .mfp-b {
border-top-width: 21px;
border-bottom-width: 21px;
opacity: 0.7; }
.mfp-arrow-left {
left: 0; }
.mfp-arrow-left:after, .mfp-arrow-left .mfp-a {
border-right: 17px solid #FFF;
margin-left: 31px; }
.mfp-arrow-left:before, .mfp-arrow-left .mfp-b {
margin-left: 25px;
border-right: 27px solid #3F3F3F; }
.mfp-arrow-right {
right: 0; }
.mfp-arrow-right:after, .mfp-arrow-right .mfp-a {
border-left: 17px solid #FFF;
margin-left: 39px; }
.mfp-arrow-right:before, .mfp-arrow-right .mfp-b {
border-left: 27px solid #3F3F3F; }
.mfp-iframe-holder {
padding-top: 40px;
padding-bottom: 40px; }
.mfp-iframe-holder .mfp-content {
line-height: 0;
width: 100%;
max-width: 900px; }
.mfp-iframe-holder .mfp-close {
top: -40px; }
.mfp-iframe-scaler {
width: 100%;
height: 0;
overflow: hidden;
padding-top: 56.25%; }
.mfp-iframe-scaler iframe {
position: absolute;
display: block;
top: 0;
left: 0;
width: 100%;
height: 100%;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
background: #000; }
/* Main image in popup */
img.mfp-img {
width: auto;
max-width: 100%;
height: auto;
display: block;
line-height: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 40px 0 40px;
margin: 0 auto; }
/* The shadow behind the image */
.mfp-figure {
line-height: 0; }
.mfp-figure:after {
content: '';
position: absolute;
left: 0;
top: 40px;
bottom: 40px;
display: block;
right: 0;
width: auto;
height: auto;
z-index: -1;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
background: #444; }
.mfp-figure small {
color: #BDBDBD;
display: block;
font-size: 12px;
line-height: 14px; }
.mfp-figure figure {
margin: 0; }
.mfp-bottom-bar {
margin-top: -36px;
position: absolute;
top: 100%;
left: 0;
width: 100%;
cursor: auto; }
.mfp-title {
text-align: left;
line-height: 18px;
color: #F3F3F3;
word-wrap: break-word;
padding-right: 36px; }
.mfp-image-holder .mfp-content {
max-width: 100%; }
.mfp-gallery .mfp-image-holder .mfp-figure {
cursor: pointer; }
@media screen and (max-width: 800px) and (orientation: landscape), screen and (max-height: 300px) {
/**
* Remove all paddings around the image on small screen
*/
.mfp-img-mobile .mfp-image-holder {
padding-left: 0;
padding-right: 0; }
.mfp-img-mobile img.mfp-img {
padding: 0; }
.mfp-img-mobile .mfp-figure:after {
top: 0;
bottom: 0; }
.mfp-img-mobile .mfp-figure small {
display: inline;
margin-left: 5px; }
.mfp-img-mobile .mfp-bottom-bar {
background: rgba(0, 0, 0, 0.6);
bottom: 0;
margin: 0;
top: auto;
padding: 3px 5px;
position: fixed;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box; }
.mfp-img-mobile .mfp-bottom-bar:empty {
padding: 0; }
.mfp-img-mobile .mfp-counter {
right: 5px;
top: 3px; }
.mfp-img-mobile .mfp-close {
top: 0;
right: 0;
width: 35px;
height: 35px;
line-height: 35px;
background: rgba(0, 0, 0, 0.6);
position: fixed;
text-align: center;
padding: 0; }
}
@media all and (max-width: 900px) {
.mfp-arrow {
-webkit-transform: scale(0.75);
transform: scale(0.75); }
.mfp-arrow-left {
-webkit-transform-origin: 0;
transform-origin: 0; }
.mfp-arrow-right {
-webkit-transform-origin: 100%;
transform-origin: 100%; }
.mfp-container {
padding-left: 6px;
padding-right: 6px; }
}
.mfp-ie7 .mfp-img {
padding: 0; }
.mfp-ie7 .mfp-bottom-bar {
width: 600px;
left: 50%;
margin-left: -300px;
margin-top: 5px;
padding-bottom: 5px; }
.mfp-ie7 .mfp-container {
padding: 0; }
.mfp-ie7 .mfp-content {
padding-top: 44px; }
.mfp-ie7 .mfp-close {
top: 0;
right: 0;
padding-top: 0; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

689
static/css/plugins/bootstrap.min.css vendored Normal file
View File

@@ -0,0 +1,689 @@
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
audio:not([controls]){display:none;}
html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
a:hover,a:active{outline:0;}
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
sup{top:-0.5em;}
sub{bottom:-0.25em;}
img{height:auto;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;}
button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
button,input{*overflow:visible;line-height:normal;}
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
textarea{overflow:auto;vertical-align:top;}
.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
.clearfix:after{clear:both;}
.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;}
a{color:#0088cc;text-decoration:none;}
a:hover{color:#005580;text-decoration:underline;}
.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
.row:after{clear:both;}
[class*="span"]{float:left;margin-left:20px;}
.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
.span12{width:940px;}
.span11{width:860px;}
.span10{width:780px;}
.span9{width:700px;}
.span8{width:620px;}
.span7{width:540px;}
.span6{width:460px;}
.span5{width:380px;}
.span4{width:300px;}
.span3{width:220px;}
.span2{width:140px;}
.span1{width:60px;}
.offset12{margin-left:980px;}
.offset11{margin-left:900px;}
.offset10{margin-left:820px;}
.offset9{margin-left:740px;}
.offset8{margin-left:660px;}
.offset7{margin-left:580px;}
.offset6{margin-left:500px;}
.offset5{margin-left:420px;}
.offset4{margin-left:340px;}
.offset3{margin-left:260px;}
.offset2{margin-left:180px;}
.offset1{margin-left:100px;}
.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
.row-fluid:after{clear:both;}
.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
.row-fluid>[class*="span"]:first-child{margin-left:0;}
.row-fluid > .span12{width:99.99999998999999%;}
.row-fluid > .span11{width:91.489361693%;}
.row-fluid > .span10{width:82.97872339599999%;}
.row-fluid > .span9{width:74.468085099%;}
.row-fluid > .span8{width:65.95744680199999%;}
.row-fluid > .span7{width:57.446808505%;}
.row-fluid > .span6{width:48.93617020799999%;}
.row-fluid > .span5{width:40.425531911%;}
.row-fluid > .span4{width:31.914893614%;}
.row-fluid > .span3{width:23.404255317%;}
.row-fluid > .span2{width:14.89361702%;}
.row-fluid > .span1{width:6.382978723%;}
.container{margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
.container:after{clear:both;}
.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
.container-fluid:after{clear:both;}
p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;}
.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;}
h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;}
h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;}
h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;}
h4,h5,h6{line-height:18px;}
h4{font-size:14px;}h4 small{font-size:12px;}
h5{font-size:12px;}
h6{font-size:11px;color:#999999;text-transform:uppercase;}
.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;}
.page-header h1{line-height:1;}
ul,ol{padding:0;margin:0 0 9px 25px;}
ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
ul{list-style:disc;}
ol{list-style:decimal;}
li{line-height:18px;}
ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
dl{margin-bottom:18px;}
dt,dd{line-height:18px;}
dt{font-weight:bold;line-height:17px;}
dd{margin-left:9px;}
.dl-horizontal dt{float:left;clear:left;width:120px;text-align:right;}
.dl-horizontal dd{margin-left:130px;}
hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
strong{font-weight:bold;}
em{font-style:italic;}
.muted{color:#999999;}
abbr[title]{border-bottom:1px dotted #ddd;cursor:help;}
abbr.initialism{font-size:90%;text-transform:uppercase;}
blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;}
blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
q:before,q:after,blockquote:before,blockquote:after{content:"";}
address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;}
small{font-size:100%;}
cite{font-style:normal;}
code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;}
pre code{padding:0;color:inherit;background-color:transparent;border:0;}
.pre-scrollable{max-height:340px;overflow-y:scroll;}
form{margin:0 0 18px;}
fieldset{padding:0;margin:0;border:0;}
legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;}
label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;}
input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
label{display:block;margin-bottom:5px;color:#333333;}
input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.uneditable-textarea{width:auto;height:auto;}
label input,label textarea,label select{display:block;}
input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;}
input[type="image"]{border:0;}
input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;}
select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;}
input[type="file"]{line-height:18px \9;}
select{width:220px;background-color:#ffffff;}
select[multiple],select[size]{height:auto;}
input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
textarea{height:auto;}
input[type="hidden"]{display:none;}
.radio,.checkbox{padding-left:18px;}
.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}
input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;}
input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
.input-mini{width:60px;}
.input-small{width:90px;}
.input-medium{width:150px;}
.input-large{width:210px;}
.input-xlarge{width:270px;}
.input-xxlarge{width:530px;}
input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;}
input,textarea,.uneditable-input{margin-left:0;}
input.span12, textarea.span12, .uneditable-input.span12{width:930px;}
input.span11, textarea.span11, .uneditable-input.span11{width:850px;}
input.span10, textarea.span10, .uneditable-input.span10{width:770px;}
input.span9, textarea.span9, .uneditable-input.span9{width:690px;}
input.span8, textarea.span8, .uneditable-input.span8{width:610px;}
input.span7, textarea.span7, .uneditable-input.span7{width:530px;}
input.span6, textarea.span6, .uneditable-input.span6{width:450px;}
input.span5, textarea.span5, .uneditable-input.span5{width:370px;}
input.span4, textarea.span4, .uneditable-input.span4{width:290px;}
input.span3, textarea.span3, .uneditable-input.span3{width:210px;}
input.span2, textarea.span2, .uneditable-input.span2{width:130px;}
input.span1, textarea.span1, .uneditable-input.span1{width:50px;}
input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#eeeeee;border-color:#ddd;cursor:not-allowed;}
.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;}
.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;}
.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;}
.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#eeeeee;border-top:1px solid #ddd;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";}
.form-actions:after{clear:both;}
.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
:-moz-placeholder{color:#999999;}
::-webkit-input-placeholder{color:#999999;}
.help-block,.help-inline{color:#555555;}
.help-block{display:block;margin-bottom:9px;}
.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
.input-prepend,.input-append{margin-bottom:5px;}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{*margin-left:0;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;}
.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;}
.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:normal;line-height:18px;text-align:center;text-shadow:0 1px 0 #ffffff;vertical-align:middle;background-color:#eeeeee;border:1px solid #ccc;}
.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;}
.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
.input-append input,.input-append select .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;}
.input-append .add-on,.input-append .btn{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;}
.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;margin-bottom:0;}
.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
.form-search label,.form-inline label{display:inline-block;}
.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px;}
.control-group{margin-bottom:9px;}
legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;}
.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";}
.form-horizontal .control-group:after{clear:both;}
.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;}
.form-horizontal .controls{margin-left:160px;*display:inline-block;*margin-left:0;*padding-left:20px;}
.form-horizontal .help-block{margin-top:9px;margin-bottom:0;}
.form-horizontal .form-actions{padding-left:160px;}
table{max-width:100%;border-collapse:collapse;border-spacing:0;background-color:transparent;}
.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
.table th{font-weight:bold;}
.table thead th{vertical-align:bottom;}
.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
.table tbody+tbody{border-top:2px solid #dddddd;}
.table-condensed th,.table-condensed td{padding:4px 5px;}
.table-bordered{border:1px solid #dddddd;border-left:0;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;}
table .span1{float:none;width:44px;margin-left:0;}
table .span2{float:none;width:124px;margin-left:0;}
table .span3{float:none;width:204px;margin-left:0;}
table .span4{float:none;width:284px;margin-left:0;}
table .span5{float:none;width:364px;margin-left:0;}
table .span6{float:none;width:444px;margin-left:0;}
table .span7{float:none;width:524px;margin-left:0;}
table .span8{float:none;width:604px;margin-left:0;}
table .span9{float:none;width:684px;margin-left:0;}
table .span10{float:none;width:764px;margin-left:0;}
table .span11{float:none;width:844px;margin-left:0;}
table .span12{float:none;width:924px;margin-left:0;}
table .span13{float:none;width:1004px;margin-left:0;}
table .span14{float:none;width:1084px;margin-left:0;}
table .span15{float:none;width:1164px;margin-left:0;}
table .span16{float:none;width:1244px;margin-left:0;}
table .span17{float:none;width:1324px;margin-left:0;}
table .span18{float:none;width:1404px;margin-left:0;}
table .span19{float:none;width:1484px;margin-left:0;}
table .span20{float:none;width:1564px;margin-left:0;}
table .span21{float:none;width:1644px;margin-left:0;}
table .span22{float:none;width:1724px;margin-left:0;}
table .span23{float:none;width:1804px;margin-left:0;}
table .span24{float:none;width:1884px;margin-left:0;}
[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;}
.icon-white{background-image:url("../img/glyphicons-halflings-white.png");}
.icon-glass{background-position:0 0;}
.icon-music{background-position:-24px 0;}
.icon-search{background-position:-48px 0;}
.icon-envelope{background-position:-72px 0;}
.icon-heart{background-position:-96px 0;}
.icon-star{background-position:-120px 0;}
.icon-star-empty{background-position:-144px 0;}
.icon-user{background-position:-168px 0;}
.icon-film{background-position:-192px 0;}
.icon-th-large{background-position:-216px 0;}
.icon-th{background-position:-240px 0;}
.icon-th-list{background-position:-264px 0;}
.icon-ok{background-position:-288px 0;}
.icon-remove{background-position:-312px 0;}
.icon-zoom-in{background-position:-336px 0;}
.icon-zoom-out{background-position:-360px 0;}
.icon-off{background-position:-384px 0;}
.icon-signal{background-position:-408px 0;}
.icon-cog{background-position:-432px 0;}
.icon-trash{background-position:-456px 0;}
.icon-home{background-position:0 -24px;}
.icon-file{background-position:-24px -24px;}
.icon-time{background-position:-48px -24px;}
.icon-road{background-position:-72px -24px;}
.icon-download-alt{background-position:-96px -24px;}
.icon-download{background-position:-120px -24px;}
.icon-upload{background-position:-144px -24px;}
.icon-inbox{background-position:-168px -24px;}
.icon-play-circle{background-position:-192px -24px;}
.icon-repeat{background-position:-216px -24px;}
.icon-refresh{background-position:-240px -24px;}
.icon-list-alt{background-position:-264px -24px;}
.icon-lock{background-position:-287px -24px;}
.icon-flag{background-position:-312px -24px;}
.icon-headphones{background-position:-336px -24px;}
.icon-volume-off{background-position:-360px -24px;}
.icon-volume-down{background-position:-384px -24px;}
.icon-volume-up{background-position:-408px -24px;}
.icon-qrcode{background-position:-432px -24px;}
.icon-barcode{background-position:-456px -24px;}
.icon-tag{background-position:0 -48px;}
.icon-tags{background-position:-25px -48px;}
.icon-book{background-position:-48px -48px;}
.icon-bookmark{background-position:-72px -48px;}
.icon-print{background-position:-96px -48px;}
.icon-camera{background-position:-120px -48px;}
.icon-font{background-position:-144px -48px;}
.icon-bold{background-position:-167px -48px;}
.icon-italic{background-position:-192px -48px;}
.icon-text-height{background-position:-216px -48px;}
.icon-text-width{background-position:-240px -48px;}
.icon-align-left{background-position:-264px -48px;}
.icon-align-center{background-position:-288px -48px;}
.icon-align-right{background-position:-312px -48px;}
.icon-align-justify{background-position:-336px -48px;}
.icon-list{background-position:-360px -48px;}
.icon-indent-left{background-position:-384px -48px;}
.icon-indent-right{background-position:-408px -48px;}
.icon-facetime-video{background-position:-432px -48px;}
.icon-picture{background-position:-456px -48px;}
.icon-pencil{background-position:0 -72px;}
.icon-map-marker{background-position:-24px -72px;}
.icon-adjust{background-position:-48px -72px;}
.icon-tint{background-position:-72px -72px;}
.icon-edit{background-position:-96px -72px;}
.icon-share{background-position:-120px -72px;}
.icon-check{background-position:-144px -72px;}
.icon-move{background-position:-168px -72px;}
.icon-step-backward{background-position:-192px -72px;}
.icon-fast-backward{background-position:-216px -72px;}
.icon-backward{background-position:-240px -72px;}
.icon-play{background-position:-264px -72px;}
.icon-pause{background-position:-288px -72px;}
.icon-stop{background-position:-312px -72px;}
.icon-forward{background-position:-336px -72px;}
.icon-fast-forward{background-position:-360px -72px;}
.icon-step-forward{background-position:-384px -72px;}
.icon-eject{background-position:-408px -72px;}
.icon-chevron-left{background-position:-432px -72px;}
.icon-chevron-right{background-position:-456px -72px;}
.icon-plus-sign{background-position:0 -96px;}
.icon-minus-sign{background-position:-24px -96px;}
.icon-remove-sign{background-position:-48px -96px;}
.icon-ok-sign{background-position:-72px -96px;}
.icon-question-sign{background-position:-96px -96px;}
.icon-info-sign{background-position:-120px -96px;}
.icon-screenshot{background-position:-144px -96px;}
.icon-remove-circle{background-position:-168px -96px;}
.icon-ok-circle{background-position:-192px -96px;}
.icon-ban-circle{background-position:-216px -96px;}
.icon-arrow-left{background-position:-240px -96px;}
.icon-arrow-right{background-position:-264px -96px;}
.icon-arrow-up{background-position:-289px -96px;}
.icon-arrow-down{background-position:-312px -96px;}
.icon-share-alt{background-position:-336px -96px;}
.icon-resize-full{background-position:-360px -96px;}
.icon-resize-small{background-position:-384px -96px;}
.icon-plus{background-position:-408px -96px;}
.icon-minus{background-position:-433px -96px;}
.icon-asterisk{background-position:-456px -96px;}
.icon-exclamation-sign{background-position:0 -120px;}
.icon-gift{background-position:-24px -120px;}
.icon-leaf{background-position:-48px -120px;}
.icon-fire{background-position:-72px -120px;}
.icon-eye-open{background-position:-96px -120px;}
.icon-eye-close{background-position:-120px -120px;}
.icon-warning-sign{background-position:-144px -120px;}
.icon-plane{background-position:-168px -120px;}
.icon-calendar{background-position:-192px -120px;}
.icon-random{background-position:-216px -120px;}
.icon-comment{background-position:-240px -120px;}
.icon-magnet{background-position:-264px -120px;}
.icon-chevron-up{background-position:-288px -120px;}
.icon-chevron-down{background-position:-313px -119px;}
.icon-retweet{background-position:-336px -120px;}
.icon-shopping-cart{background-position:-360px -120px;}
.icon-folder-close{background-position:-384px -120px;}
.icon-folder-open{background-position:-408px -120px;}
.icon-resize-vertical{background-position:-432px -119px;}
.icon-resize-horizontal{background-position:-456px -118px;}
.dropdown{position:relative;}
.dropdown-toggle{*margin-bottom:-3px;}
.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
.caret{display:inline-block;width:0;height:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"";}
.dropdown .caret{margin-top:8px;margin-left:2px;}
.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);}
.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.pull-right{right:0;left:auto;}
.dropdown-menu .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#333333;white-space:nowrap;}
.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;}
.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
.dropdown.open .dropdown-menu{display:block;}
.pull-right .dropdown-menu{left:auto;right:0;}
.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"\2191";}
.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;}
.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;}
.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #cccccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;}
.btn:active,.btn.active{background-color:#cccccc \9;}
.btn:first-child{*margin-left:0;}
.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;}
.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.btn-large [class^="icon-"]{margin-top:1px;}
.btn-small{padding:5px 9px;font-size:11px;line-height:16px;}
.btn-small [class^="icon-"]{margin-top:-1px;}
.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;}
.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
.btn-primary{background-color:#0074cc;background-image:-moz-linear-gradient(top, #0088cc, #0055cc);background-image:-ms-linear-gradient(top, #0088cc, #0055cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc));background-image:-webkit-linear-gradient(top, #0088cc, #0055cc);background-image:-o-linear-gradient(top, #0088cc, #0055cc);background-image:linear-gradient(top, #0088cc, #0055cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0);border-color:#0055cc #0055cc #003580;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0055cc;}
.btn-primary:active,.btn-primary.active{background-color:#004099 \9;}
.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;}
.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;}
.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;}
.btn-success:active,.btn-success.active{background-color:#408140 \9;}
.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;}
.btn-info:active,.btn-info.active{background-color:#24748c \9;}
.btn-inverse{background-color:#414141;background-image:-moz-linear-gradient(top, #555555, #222222);background-image:-ms-linear-gradient(top, #555555, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222));background-image:-webkit-linear-gradient(top, #555555, #222222);background-image:-o-linear-gradient(top, #555555, #222222);background-image:linear-gradient(top, #555555, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222222;}
.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";}
.btn-group:after{clear:both;}
.btn-group:first-child{*margin-left:0;}
.btn-group+.btn-group{margin-left:5px;}
.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;}
.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:3px;*padding-bottom:3px;}
.btn-group .btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:1px;*padding-bottom:1px;}
.btn-group .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;}
.btn-group .btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;}
.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);}
.btn .caret{margin-top:7px;margin-left:0;}
.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);}
.btn-mini .caret{margin-top:5px;}
.btn-small .caret{margin-top:6px;}
.btn-large .caret{margin-top:6px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);}
.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
.alert-heading{color:inherit;}
.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;}
.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
.alert-block{padding-top:14px;padding-bottom:14px;}
.alert-block>p,.alert-block>ul{margin-bottom:0;}
.alert-block p+p{margin-top:5px;}
.nav{margin-left:0;margin-bottom:18px;list-style:none;}
.nav>li>a{display:block;}
.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
.nav li+.nav-header{margin-top:9px;}
.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
.nav-list>li>a{padding:3px 15px;}
.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
.nav-list [class^="icon-"]{margin-right:2px;}
.nav-list .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
.nav-tabs:after,.nav-pills:after{clear:both;}
.nav-tabs>li,.nav-pills>li{float:left;}
.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
.nav-tabs{border-bottom:1px solid #ddd;}
.nav-tabs>li{margin-bottom:-1px;}
.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;}
.nav-stacked>li{float:none;}
.nav-stacked>li>a{margin-right:0;}
.nav-tabs.nav-stacked{border-bottom:0;}
.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;}
.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;}
.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;}
.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
.tabs-stacked .open>a:hover{border-color:#999999;}
.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
.tabbable:after{clear:both;}
.tab-content{display:table;width:100%;}
.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
.tab-content>.active,.pill-content>.active{display:block;}
.tabs-below .nav-tabs{border-top:1px solid #ddd;}
.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
.navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:18px;}
.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
.navbar .container{width:auto;}
.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;}
.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;}
.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
.nav-collapse.collapse{height:auto;}
.navbar{color:#999999;}.navbar .brand:hover{text-decoration:none;}
.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
.navbar .navbar-text{margin-bottom:0;line-height:40px;}
.navbar .btn,.navbar .btn-group{margin-top:5px;}
.navbar .btn-group .btn{margin-top:0;}
.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
.navbar-form:after{clear:both;}
.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;}
.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;}
.navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
.navbar-fixed-top{top:0;}
.navbar-fixed-bottom{bottom:0;}
.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
.navbar .nav.pull-right{float:right;}
.navbar .nav>li{display:block;float:left;}
.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;}
.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;}
.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
.navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
.navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
.navbar .nav.pull-right .dropdown-menu,.navbar .nav .dropdown-menu.pull-right{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before,.navbar .nav .dropdown-menu.pull-right:before{left:auto;right:12px;}
.navbar .nav.pull-right .dropdown-menu:after,.navbar .nav .dropdown-menu.pull-right:after{left:auto;right:13px;}
.breadcrumb{padding:7px 14px;margin:0 0 18px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
.breadcrumb .divider{padding:0 5px;color:#999999;}
.breadcrumb .active a{color:#333333;}
.pagination{height:36px;margin:18px 0;}
.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
.pagination li{display:inline;}
.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;}
.pagination a:hover,.pagination .active a{background-color:#f5f5f5;}
.pagination .active a{color:#999999;cursor:default;}
.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;}
.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.pagination-centered{text-align:center;}
.pagination-right{text-align:right;}
.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";}
.pager:after{clear:both;}
.pager li{display:inline;}
.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
.pager .next a{float:right;}
.pager .previous a{float:left;}
.pager .disabled a,.pager .disabled a:hover{color:#999999;background-color:#fff;cursor:default;}
.modal-open .dropdown-menu{z-index:2050;}
.modal-open .dropdown.open{*z-index:2050;}
.modal-open .popover{z-index:2060;}
.modal-open .tooltip{z-index:2070;}
.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
.modal.fade.in{top:50%;}
.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
.modal-form{margin-bottom:0;}
.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";}
.modal-footer:after{clear:both;}
.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
.tooltip.top{margin-top:-2px;}
.tooltip.right{margin-left:2px;}
.tooltip.bottom{margin-top:2px;}
.tooltip.left{margin-left:-2px;}
.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.tooltip-arrow{position:absolute;width:0;height:0;}
.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;}
.popover.right{margin-left:5px;}
.popover.bottom{margin-top:5px;}
.popover.left{margin-left:-5px;}
.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.popover .arrow{position:absolute;width:0;height:0;}
.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}
.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";}
.thumbnails:after{clear:both;}
.thumbnails>li{float:left;margin:0 0 18px 20px;}
.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}
a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
.thumbnail .caption{padding:9px;}
.label{padding:1px 4px 2px;font-size:10.998px;font-weight:bold;line-height:13px;color:#ffffff;vertical-align:middle;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
.label:hover{color:#ffffff;text-decoration:none;}
.label-important{background-color:#b94a48;}
.label-important:hover{background-color:#953b39;}
.label-warning{background-color:#f89406;}
.label-warning:hover{background-color:#c67605;}
.label-success{background-color:#468847;}
.label-success:hover{background-color:#356635;}
.label-info{background-color:#3a87ad;}
.label-info:hover{background-color:#2d6987;}
.label-inverse{background-color:#333333;}
.label-inverse:hover{background-color:#1a1a1a;}
.badge{padding:1px 9px 2px;font-size:12.025px;font-weight:bold;white-space:nowrap;color:#ffffff;background-color:#999999;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
.badge-error{background-color:#b94a48;}
.badge-error:hover{background-color:#953b39;}
.badge-warning{background-color:#f89406;}
.badge-warning:hover{background-color:#c67605;}
.badge-success{background-color:#468847;}
.badge-success:hover{background-color:#356635;}
.badge-info{background-color:#3a87ad;}
.badge-info:hover{background-color:#2d6987;}
.badge-inverse{background-color:#333333;}
.badge-inverse:hover{background-color:#1a1a1a;}
@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);}
.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);}
.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);}
.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);}
.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
.accordion{margin-bottom:18px;}
.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.accordion-heading{border-bottom:0;}
.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
.carousel{position:relative;margin-bottom:18px;line-height:1;}
.carousel-inner{overflow:hidden;width:100%;position:relative;}
.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
.carousel .item>img{display:block;line-height:1;}
.carousel .active,.carousel .next,.carousel .prev{display:block;}
.carousel .active{left:0;}
.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
.carousel .next{left:100%;}
.carousel .prev{left:-100%;}
.carousel .next.left,.carousel .prev.right{left:0;}
.carousel .active.left{left:-100%;}
.carousel .active.right{left:100%;}
.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);}
.carousel-caption h4,.carousel-caption p{color:#ffffff;}
.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
.hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit;}
.pull-right{float:right;}
.pull-left{float:left;}
.hide{display:none;}
.show{display:block;}
.invisible{visibility:hidden;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

View File

@@ -0,0 +1,429 @@
/*!
Chosen, a Select Box Enhancer for jQuery and Prototype
by Patrick Filler for Harvest, http://getharvest.com
Version 1.1.0
Full source at https://github.com/harvesthq/chosen
Copyright (c) 2011 Harvest http://getharvest.com
MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
This file is generated by `grunt build`, do not edit it by hand.
*/
/* @group Base */
.chosen-container {
position: relative;
display: inline-block;
vertical-align: middle;
font-size: 13px;
zoom: 1;
*display: inline;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.chosen-container .chosen-drop {
position: absolute;
top: 100%;
left: -9999px;
z-index: 1010;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
border: 1px solid #aaa;
border-top: 0;
background: #fff;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
}
.chosen-container.chosen-with-drop .chosen-drop {
left: 0;
}
.chosen-container a {
cursor: pointer;
}
/* @end */
/* @group Single Chosen */
.chosen-container-single .chosen-single {
position: relative;
display: block;
overflow: hidden;
padding: 0 0 0 8px;
height: 23px;
border: 1px solid #aaa;
border-radius: 5px;
background-color: #fff;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
background: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background-clip: padding-box;
box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1);
color: #444;
text-decoration: none;
white-space: nowrap;
line-height: 24px;
}
.chosen-container-single .chosen-default {
color: #999;
}
.chosen-container-single .chosen-single span {
display: block;
overflow: hidden;
margin-right: 26px;
text-overflow: ellipsis;
white-space: nowrap;
}
.chosen-container-single .chosen-single-with-deselect span {
margin-right: 38px;
}
.chosen-container-single .chosen-single abbr {
position: absolute;
top: 6px;
right: 26px;
display: block;
width: 12px;
height: 12px;
background: url('chosen-sprite.png') -42px 1px no-repeat;
font-size: 1px;
}
.chosen-container-single .chosen-single abbr:hover {
background-position: -42px -10px;
}
.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
background-position: -42px -10px;
}
.chosen-container-single .chosen-single div {
position: absolute;
top: 0;
right: 0;
display: block;
width: 18px;
height: 100%;
}
.chosen-container-single .chosen-single div b {
display: block;
width: 100%;
height: 100%;
background: url('chosen-sprite.png') no-repeat 0px 2px;
}
.chosen-container-single .chosen-search {
position: relative;
z-index: 1010;
margin: 0;
padding: 3px 4px;
white-space: nowrap;
}
.chosen-container-single .chosen-search input[type="text"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1px 0;
padding: 4px 20px 4px 5px;
width: 100%;
height: auto;
outline: 0;
border: 1px solid #aaa;
background: white url('chosen-sprite.png') no-repeat 100% -20px;
background: url('chosen-sprite.png') no-repeat 100% -20px;
font-size: 1em;
font-family: sans-serif;
line-height: normal;
border-radius: 0;
}
.chosen-container-single .chosen-drop {
margin-top: -1px;
border-radius: 0 0 4px 4px;
background-clip: padding-box;
}
.chosen-container-single.chosen-container-single-nosearch .chosen-search {
position: absolute;
left: -9999px;
}
/* @end */
/* @group Results */
.chosen-container .chosen-results {
position: relative;
overflow-x: hidden;
overflow-y: auto;
margin: 0 4px 4px 0;
padding: 0 0 0 4px;
max-height: 240px;
-webkit-overflow-scrolling: touch;
}
.chosen-container .chosen-results li {
display: none;
margin: 0;
padding: 5px 6px;
list-style: none;
line-height: 15px;
-webkit-touch-callout: none;
}
.chosen-container .chosen-results li.active-result {
display: list-item;
cursor: pointer;
}
.chosen-container .chosen-results li.disabled-result {
display: list-item;
color: #ccc;
cursor: default;
}
.chosen-container .chosen-results li.highlighted {
background-color: #3875d7;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
color: #fff;
}
.chosen-container .chosen-results li.no-results {
display: list-item;
background: #f4f4f4;
}
.chosen-container .chosen-results li.group-result {
display: list-item;
font-weight: bold;
cursor: default;
}
.chosen-container .chosen-results li.group-option {
padding-left: 15px;
}
.chosen-container .chosen-results li em {
font-style: normal;
text-decoration: underline;
}
/* @end */
/* @group Multi Chosen */
.chosen-container-multi .chosen-choices {
-moz-box-sizing: border-box;
background-color: #FFFFFF;
border: 1px solid #CBD5DD;
border-radius: 2px;
cursor: text;
height: auto !important;
margin: 0;
min-height: 30px;
overflow: hidden;
padding: 2px;
position: relative;
width: 100%;
}
.chosen-container-multi .chosen-choices li {
float: left;
list-style: none;
}
.chosen-container-multi .chosen-choices li.search-field {
margin: 0;
padding: 0;
white-space: nowrap;
}
.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
margin: 1px 0;
padding: 5px;
height: 25px;
outline: 0;
border: 0 !important;
background: transparent !important;
box-shadow: none;
color: #666;
font-size: 100%;
font-family: sans-serif;
line-height: normal;
border-radius: 0;
}
.chosen-container-multi .chosen-choices li.search-field .default {
color: #999;
}
.chosen-container-multi .chosen-choices li.search-choice {
position: relative;
margin: 3px 0 3px 5px;
padding: 3px 20px 3px 5px;
border: 1px solid #aaa;
border-radius: 3px;
background-color: #e4e4e4;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-clip: padding-box;
box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05);
color: #333;
line-height: 13px;
cursor: default;
}
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
position: absolute;
top: 4px;
right: 3px;
display: block;
width: 12px;
height: 12px;
background: url('chosen-sprite.png') -42px 1px no-repeat;
font-size: 1px;
}
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
background-position: -42px -10px;
}
.chosen-container-multi .chosen-choices li.search-choice-disabled {
padding-right: 5px;
border: 1px solid #ccc;
background-color: #e4e4e4;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
color: #666;
}
.chosen-container-multi .chosen-choices li.search-choice-focus {
background: #d4d4d4;
}
.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
background-position: -42px -10px;
}
.chosen-container-multi .chosen-results {
margin: 0;
padding: 0;
}
.chosen-container-multi .chosen-drop .result-selected {
display: list-item;
color: #ccc;
cursor: default;
}
/* @end */
/* @group Active */
.chosen-container-active .chosen-single {
border: 1px solid #5897fb;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.chosen-container-active.chosen-with-drop .chosen-single {
border: 1px solid #aaa;
-moz-border-radius-bottomright: 0;
border-bottom-right-radius: 0;
-moz-border-radius-bottomleft: 0;
border-bottom-left-radius: 0;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
box-shadow: 0 1px 0 #fff inset;
}
.chosen-container-active.chosen-with-drop .chosen-single div {
border-left: none;
background: transparent;
}
.chosen-container-active.chosen-with-drop .chosen-single div b {
background-position: -18px 2px;
}
.chosen-container-active .chosen-choices {
border: 1px solid #5897fb;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.chosen-container-active .chosen-choices li.search-field input[type="text"] {
color: #111 !important;
}
/* @end */
/* @group Disabled Support */
.chosen-disabled {
opacity: 0.5 !important;
cursor: default;
}
.chosen-disabled .chosen-single {
cursor: default;
}
.chosen-disabled .chosen-choices .search-choice .search-choice-close {
cursor: default;
}
/* @end */
/* @group Right to Left */
.chosen-rtl {
text-align: right;
}
.chosen-rtl .chosen-single {
overflow: visible;
padding: 0 8px 0 0;
}
.chosen-rtl .chosen-single span {
margin-right: 0;
margin-left: 26px;
direction: rtl;
}
.chosen-rtl .chosen-single-with-deselect span {
margin-left: 38px;
}
.chosen-rtl .chosen-single div {
right: auto;
left: 3px;
}
.chosen-rtl .chosen-single abbr {
right: auto;
left: 26px;
}
.chosen-rtl .chosen-choices li {
float: right;
}
.chosen-rtl .chosen-choices li.search-field input[type="text"] {
direction: rtl;
}
.chosen-rtl .chosen-choices li.search-choice {
margin: 3px 5px 3px 0;
padding: 3px 5px 3px 19px;
}
.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
right: auto;
left: 4px;
}
.chosen-rtl.chosen-container-single-nosearch .chosen-search,
.chosen-rtl .chosen-drop {
left: 9999px;
}
.chosen-rtl.chosen-container-single .chosen-results {
margin: 0 0 4px 4px;
padding: 0 4px 0 0;
}
.chosen-rtl .chosen-results li.group-option {
padding-right: 15px;
padding-left: 0;
}
.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
border-right: none;
}
.chosen-rtl .chosen-search input[type="text"] {
padding: 4px 5px 4px 20px;
background: white url('chosen-sprite.png') no-repeat -30px -20px;
background: url('chosen-sprite.png') no-repeat -30px -20px;
direction: rtl;
}
.chosen-rtl.chosen-container-single .chosen-single div b {
background-position: 6px 2px;
}
.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
background-position: -12px 2px;
}
/* @end */
/* @group Retina compatibility */
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) {
.chosen-rtl .chosen-search input[type="text"],
.chosen-container-single .chosen-single abbr,
.chosen-container-single .chosen-single div b,
.chosen-container-single .chosen-search input[type="text"],
.chosen-container-multi .chosen-choices .search-choice .search-choice-close,
.chosen-container .chosen-results-scroll-down span,
.chosen-container .chosen-results-scroll-up span {
background-image: url('chosen-sprite@2x.png') !important;
background-size: 52px 37px !important;
background-repeat: no-repeat !important;
}
}
/* @end */

View File

@@ -0,0 +1,789 @@
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Improvements by Andrew Rowls
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
padding: 4px;
border-radius: 4px;
direction: ltr;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker-inline {
width: 220px;
}
.datepicker.datepicker-rtl {
direction: rtl;
}
.datepicker.datepicker-rtl table tr td span {
float: right;
}
.datepicker-dropdown {
top: 0;
left: 0;
}
.datepicker-dropdown:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-top: 0;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
}
.datepicker-dropdown:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
border-top: 0;
position: absolute;
}
.datepicker-dropdown.datepicker-orient-left:before {
left: 6px;
}
.datepicker-dropdown.datepicker-orient-left:after {
left: 7px;
}
.datepicker-dropdown.datepicker-orient-right:before {
right: 6px;
}
.datepicker-dropdown.datepicker-orient-right:after {
right: 7px;
}
.datepicker-dropdown.datepicker-orient-top:before {
top: -7px;
}
.datepicker-dropdown.datepicker-orient-top:after {
top: -6px;
}
.datepicker-dropdown.datepicker-orient-bottom:before {
bottom: -7px;
border-bottom: 0;
border-top: 7px solid #999;
}
.datepicker-dropdown.datepicker-orient-bottom:after {
bottom: -6px;
border-bottom: 0;
border-top: 6px solid #fff;
}
.datepicker > div {
display: none;
}
.datepicker.days div.datepicker-days {
display: block;
}
.datepicker.months div.datepicker-months {
display: block;
}
.datepicker.years div.datepicker-years {
display: block;
}
.datepicker table {
margin: 0;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.datepicker table tr td,
.datepicker table tr th {
text-align: center;
width: 30px;
height: 30px;
border-radius: 4px;
border: none;
}
.table-striped .datepicker table tr td,
.table-striped .datepicker table tr th {
background-color: transparent;
}
.datepicker table tr td.day:hover,
.datepicker table tr td.day.focused {
background: #eeeeee;
cursor: pointer;
}
.datepicker table tr td.old,
.datepicker table tr td.new {
color: #999999;
}
.datepicker table tr td.disabled,
.datepicker table tr td.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td.today,
.datepicker table tr td.today:hover,
.datepicker table tr td.today.disabled,
.datepicker table tr td.today.disabled:hover {
color: #000000;
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:hover,
.datepicker table tr td.today:hover:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today:focus,
.datepicker table tr td.today:hover:focus,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today.disabled:hover:focus,
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.today,
.open .dropdown-toggle.datepicker table tr td.today:hover,
.open .dropdown-toggle.datepicker table tr td.today.disabled,
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
color: #000000;
background-color: #ffcd70;
border-color: #f59e00;
}
.datepicker table tr td.today:active,
.datepicker table tr td.today:hover:active,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.active,
.datepicker table tr td.today:hover.active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.today,
.open .dropdown-toggle.datepicker table tr td.today:hover,
.open .dropdown-toggle.datepicker table tr td.today.disabled,
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
background-image: none;
}
.datepicker table tr td.today.disabled,
.datepicker table tr td.today:hover.disabled,
.datepicker table tr td.today.disabled.disabled,
.datepicker table tr td.today.disabled:hover.disabled,
.datepicker table tr td.today[disabled],
.datepicker table tr td.today:hover[disabled],
.datepicker table tr td.today.disabled[disabled],
.datepicker table tr td.today.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.today,
fieldset[disabled] .datepicker table tr td.today:hover,
fieldset[disabled] .datepicker table tr td.today.disabled,
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today.disabled:hover,
.datepicker table tr td.today:hover.disabled:hover,
.datepicker table tr td.today.disabled.disabled:hover,
.datepicker table tr td.today.disabled:hover.disabled:hover,
.datepicker table tr td.today[disabled]:hover,
.datepicker table tr td.today:hover[disabled]:hover,
.datepicker table tr td.today.disabled[disabled]:hover,
.datepicker table tr td.today.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.today:hover,
fieldset[disabled] .datepicker table tr td.today:hover:hover,
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:hover,
.datepicker table tr td.today.disabled:focus,
.datepicker table tr td.today:hover.disabled:focus,
.datepicker table tr td.today.disabled.disabled:focus,
.datepicker table tr td.today.disabled:hover.disabled:focus,
.datepicker table tr td.today[disabled]:focus,
.datepicker table tr td.today:hover[disabled]:focus,
.datepicker table tr td.today.disabled[disabled]:focus,
.datepicker table tr td.today.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.today:focus,
fieldset[disabled] .datepicker table tr td.today:hover:focus,
fieldset[disabled] .datepicker table tr td.today.disabled:focus,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:focus,
.datepicker table tr td.today.disabled:active,
.datepicker table tr td.today:hover.disabled:active,
.datepicker table tr td.today.disabled.disabled:active,
.datepicker table tr td.today.disabled:hover.disabled:active,
.datepicker table tr td.today[disabled]:active,
.datepicker table tr td.today:hover[disabled]:active,
.datepicker table tr td.today.disabled[disabled]:active,
.datepicker table tr td.today.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.today:active,
fieldset[disabled] .datepicker table tr td.today:hover:active,
fieldset[disabled] .datepicker table tr td.today.disabled:active,
fieldset[disabled] .datepicker table tr td.today.disabled:hover:active,
.datepicker table tr td.today.disabled.active,
.datepicker table tr td.today:hover.disabled.active,
.datepicker table tr td.today.disabled.disabled.active,
.datepicker table tr td.today.disabled:hover.disabled.active,
.datepicker table tr td.today[disabled].active,
.datepicker table tr td.today:hover[disabled].active,
.datepicker table tr td.today.disabled[disabled].active,
.datepicker table tr td.today.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.today.active,
fieldset[disabled] .datepicker table tr td.today:hover.active,
fieldset[disabled] .datepicker table tr td.today.disabled.active,
fieldset[disabled] .datepicker table tr td.today.disabled:hover.active {
background-color: #ffdb99;
border-color: #ffb733;
}
.datepicker table tr td.today:hover:hover {
color: #000;
}
.datepicker table tr td.today.active:hover {
color: #fff;
}
.datepicker table tr td.range,
.datepicker table tr td.range:hover,
.datepicker table tr td.range.disabled,
.datepicker table tr td.range.disabled:hover {
background: #eeeeee;
border-radius: 0;
}
.datepicker table tr td.range.today,
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today.disabled:hover {
color: #000000;
background-color: #f7ca77;
border-color: #f1a417;
border-radius: 0;
}
.datepicker table tr td.range.today:hover,
.datepicker table tr td.range.today:hover:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today:focus,
.datepicker table tr td.range.today:hover:focus,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today.disabled:hover:focus,
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.range.today,
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
color: #000000;
background-color: #f4bb51;
border-color: #bf800c;
}
.datepicker table tr td.range.today:active,
.datepicker table tr td.range.today:hover:active,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.active,
.datepicker table tr td.range.today:hover.active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.range.today,
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
background-image: none;
}
.datepicker table tr td.range.today.disabled,
.datepicker table tr td.range.today:hover.disabled,
.datepicker table tr td.range.today.disabled.disabled,
.datepicker table tr td.range.today.disabled:hover.disabled,
.datepicker table tr td.range.today[disabled],
.datepicker table tr td.range.today:hover[disabled],
.datepicker table tr td.range.today.disabled[disabled],
.datepicker table tr td.range.today.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.range.today,
fieldset[disabled] .datepicker table tr td.range.today:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today.disabled:hover,
.datepicker table tr td.range.today:hover.disabled:hover,
.datepicker table tr td.range.today.disabled.disabled:hover,
.datepicker table tr td.range.today.disabled:hover.disabled:hover,
.datepicker table tr td.range.today[disabled]:hover,
.datepicker table tr td.range.today:hover[disabled]:hover,
.datepicker table tr td.range.today.disabled[disabled]:hover,
.datepicker table tr td.range.today.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover,
fieldset[disabled] .datepicker table tr td.range.today:hover:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:hover,
.datepicker table tr td.range.today.disabled:focus,
.datepicker table tr td.range.today:hover.disabled:focus,
.datepicker table tr td.range.today.disabled.disabled:focus,
.datepicker table tr td.range.today.disabled:hover.disabled:focus,
.datepicker table tr td.range.today[disabled]:focus,
.datepicker table tr td.range.today:hover[disabled]:focus,
.datepicker table tr td.range.today.disabled[disabled]:focus,
.datepicker table tr td.range.today.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.range.today:focus,
fieldset[disabled] .datepicker table tr td.range.today:hover:focus,
fieldset[disabled] .datepicker table tr td.range.today.disabled:focus,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:focus,
.datepicker table tr td.range.today.disabled:active,
.datepicker table tr td.range.today:hover.disabled:active,
.datepicker table tr td.range.today.disabled.disabled:active,
.datepicker table tr td.range.today.disabled:hover.disabled:active,
.datepicker table tr td.range.today[disabled]:active,
.datepicker table tr td.range.today:hover[disabled]:active,
.datepicker table tr td.range.today.disabled[disabled]:active,
.datepicker table tr td.range.today.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.range.today:active,
fieldset[disabled] .datepicker table tr td.range.today:hover:active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:active,
.datepicker table tr td.range.today.disabled.active,
.datepicker table tr td.range.today:hover.disabled.active,
.datepicker table tr td.range.today.disabled.disabled.active,
.datepicker table tr td.range.today.disabled:hover.disabled.active,
.datepicker table tr td.range.today[disabled].active,
.datepicker table tr td.range.today:hover[disabled].active,
.datepicker table tr td.range.today.disabled[disabled].active,
.datepicker table tr td.range.today.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.range.today.active,
fieldset[disabled] .datepicker table tr td.range.today:hover.active,
fieldset[disabled] .datepicker table tr td.range.today.disabled.active,
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover.active {
background-color: #f7ca77;
border-color: #f1a417;
}
.datepicker table tr td.selected,
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected.disabled:hover {
color: #ffffff;
background-color: #999999;
border-color: #555555;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.selected:hover,
.datepicker table tr td.selected:hover:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected:focus,
.datepicker table tr td.selected:hover:focus,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected.disabled:hover:focus,
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.selected,
.open .dropdown-toggle.datepicker table tr td.selected:hover,
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
color: #ffffff;
background-color: #858585;
border-color: #373737;
}
.datepicker table tr td.selected:active,
.datepicker table tr td.selected:hover:active,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.active,
.datepicker table tr td.selected:hover.active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.selected,
.open .dropdown-toggle.datepicker table tr td.selected:hover,
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
background-image: none;
}
.datepicker table tr td.selected.disabled,
.datepicker table tr td.selected:hover.disabled,
.datepicker table tr td.selected.disabled.disabled,
.datepicker table tr td.selected.disabled:hover.disabled,
.datepicker table tr td.selected[disabled],
.datepicker table tr td.selected:hover[disabled],
.datepicker table tr td.selected.disabled[disabled],
.datepicker table tr td.selected.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.selected,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected.disabled:hover,
.datepicker table tr td.selected:hover.disabled:hover,
.datepicker table tr td.selected.disabled.disabled:hover,
.datepicker table tr td.selected.disabled:hover.disabled:hover,
.datepicker table tr td.selected[disabled]:hover,
.datepicker table tr td.selected:hover[disabled]:hover,
.datepicker table tr td.selected.disabled[disabled]:hover,
.datepicker table tr td.selected.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.selected:hover,
fieldset[disabled] .datepicker table tr td.selected:hover:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:hover,
.datepicker table tr td.selected.disabled:focus,
.datepicker table tr td.selected:hover.disabled:focus,
.datepicker table tr td.selected.disabled.disabled:focus,
.datepicker table tr td.selected.disabled:hover.disabled:focus,
.datepicker table tr td.selected[disabled]:focus,
.datepicker table tr td.selected:hover[disabled]:focus,
.datepicker table tr td.selected.disabled[disabled]:focus,
.datepicker table tr td.selected.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.selected:focus,
fieldset[disabled] .datepicker table tr td.selected:hover:focus,
fieldset[disabled] .datepicker table tr td.selected.disabled:focus,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:focus,
.datepicker table tr td.selected.disabled:active,
.datepicker table tr td.selected:hover.disabled:active,
.datepicker table tr td.selected.disabled.disabled:active,
.datepicker table tr td.selected.disabled:hover.disabled:active,
.datepicker table tr td.selected[disabled]:active,
.datepicker table tr td.selected:hover[disabled]:active,
.datepicker table tr td.selected.disabled[disabled]:active,
.datepicker table tr td.selected.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.selected:active,
fieldset[disabled] .datepicker table tr td.selected:hover:active,
fieldset[disabled] .datepicker table tr td.selected.disabled:active,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:active,
.datepicker table tr td.selected.disabled.active,
.datepicker table tr td.selected:hover.disabled.active,
.datepicker table tr td.selected.disabled.disabled.active,
.datepicker table tr td.selected.disabled:hover.disabled.active,
.datepicker table tr td.selected[disabled].active,
.datepicker table tr td.selected:hover[disabled].active,
.datepicker table tr td.selected.disabled[disabled].active,
.datepicker table tr td.selected.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.selected.active,
fieldset[disabled] .datepicker table tr td.selected:hover.active,
fieldset[disabled] .datepicker table tr td.selected.disabled.active,
fieldset[disabled] .datepicker table tr td.selected.disabled:hover.active {
background-color: #999999;
border-color: #555555;
}
.datepicker table tr td.active,
.datepicker table tr td.active:hover,
.datepicker table tr td.active.disabled,
.datepicker table tr td.active.disabled:hover {
color: #ffffff;
background-color: #428bca;
border-color: #357ebd;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td.active:hover,
.datepicker table tr td.active:hover:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active:focus,
.datepicker table tr td.active:hover:focus,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active.disabled:hover:focus,
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.active,
.open .dropdown-toggle.datepicker table tr td.active:hover,
.open .dropdown-toggle.datepicker table tr td.active.disabled,
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
color: #ffffff;
background-color: #3276b1;
border-color: #285e8e;
}
.datepicker table tr td.active:active,
.datepicker table tr td.active:hover:active,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.active,
.datepicker table tr td.active:hover.active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td.active,
.open .dropdown-toggle.datepicker table tr td.active:hover,
.open .dropdown-toggle.datepicker table tr td.active.disabled,
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
background-image: none;
}
.datepicker table tr td.active.disabled,
.datepicker table tr td.active:hover.disabled,
.datepicker table tr td.active.disabled.disabled,
.datepicker table tr td.active.disabled:hover.disabled,
.datepicker table tr td.active[disabled],
.datepicker table tr td.active:hover[disabled],
.datepicker table tr td.active.disabled[disabled],
.datepicker table tr td.active.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td.active,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active.disabled,
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active.disabled:hover,
.datepicker table tr td.active:hover.disabled:hover,
.datepicker table tr td.active.disabled.disabled:hover,
.datepicker table tr td.active.disabled:hover.disabled:hover,
.datepicker table tr td.active[disabled]:hover,
.datepicker table tr td.active:hover[disabled]:hover,
.datepicker table tr td.active.disabled[disabled]:hover,
.datepicker table tr td.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td.active:hover,
fieldset[disabled] .datepicker table tr td.active:hover:hover,
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:hover,
.datepicker table tr td.active.disabled:focus,
.datepicker table tr td.active:hover.disabled:focus,
.datepicker table tr td.active.disabled.disabled:focus,
.datepicker table tr td.active.disabled:hover.disabled:focus,
.datepicker table tr td.active[disabled]:focus,
.datepicker table tr td.active:hover[disabled]:focus,
.datepicker table tr td.active.disabled[disabled]:focus,
.datepicker table tr td.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td.active:focus,
fieldset[disabled] .datepicker table tr td.active:hover:focus,
fieldset[disabled] .datepicker table tr td.active.disabled:focus,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:focus,
.datepicker table tr td.active.disabled:active,
.datepicker table tr td.active:hover.disabled:active,
.datepicker table tr td.active.disabled.disabled:active,
.datepicker table tr td.active.disabled:hover.disabled:active,
.datepicker table tr td.active[disabled]:active,
.datepicker table tr td.active:hover[disabled]:active,
.datepicker table tr td.active.disabled[disabled]:active,
.datepicker table tr td.active.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td.active:active,
fieldset[disabled] .datepicker table tr td.active:hover:active,
fieldset[disabled] .datepicker table tr td.active.disabled:active,
fieldset[disabled] .datepicker table tr td.active.disabled:hover:active,
.datepicker table tr td.active.disabled.active,
.datepicker table tr td.active:hover.disabled.active,
.datepicker table tr td.active.disabled.disabled.active,
.datepicker table tr td.active.disabled:hover.disabled.active,
.datepicker table tr td.active[disabled].active,
.datepicker table tr td.active:hover[disabled].active,
.datepicker table tr td.active.disabled[disabled].active,
.datepicker table tr td.active.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td.active.active,
fieldset[disabled] .datepicker table tr td.active:hover.active,
fieldset[disabled] .datepicker table tr td.active.disabled.active,
fieldset[disabled] .datepicker table tr td.active.disabled:hover.active {
background-color: #428bca;
border-color: #357ebd;
}
.datepicker table tr td span {
display: block;
width: 23%;
height: 54px;
line-height: 54px;
float: left;
margin: 1%;
cursor: pointer;
border-radius: 4px;
}
.datepicker table tr td span:hover {
background: #eeeeee;
}
.datepicker table tr td span.disabled,
.datepicker table tr td span.disabled:hover {
background: none;
color: #999999;
cursor: default;
}
.datepicker table tr td span.active,
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active.disabled:hover {
color: #ffffff;
background-color: #428bca;
border-color: #357ebd;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker table tr td span.active:hover,
.datepicker table tr td span.active:hover:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active:focus,
.datepicker table tr td span.active:hover:focus,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td span.active,
.open .dropdown-toggle.datepicker table tr td span.active:hover,
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
color: #ffffff;
background-color: #3276b1;
border-color: #285e8e;
}
.datepicker table tr td span.active:active,
.datepicker table tr td span.active:hover:active,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.active,
.datepicker table tr td span.active:hover.active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active.disabled:hover.active,
.open .dropdown-toggle.datepicker table tr td span.active,
.open .dropdown-toggle.datepicker table tr td span.active:hover,
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
background-image: none;
}
.datepicker table tr td span.active.disabled,
.datepicker table tr td span.active:hover.disabled,
.datepicker table tr td span.active.disabled.disabled,
.datepicker table tr td span.active.disabled:hover.disabled,
.datepicker table tr td span.active[disabled],
.datepicker table tr td span.active:hover[disabled],
.datepicker table tr td span.active.disabled[disabled],
.datepicker table tr td span.active.disabled:hover[disabled],
fieldset[disabled] .datepicker table tr td span.active,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active.disabled:hover,
.datepicker table tr td span.active:hover.disabled:hover,
.datepicker table tr td span.active.disabled.disabled:hover,
.datepicker table tr td span.active.disabled:hover.disabled:hover,
.datepicker table tr td span.active[disabled]:hover,
.datepicker table tr td span.active:hover[disabled]:hover,
.datepicker table tr td span.active.disabled[disabled]:hover,
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
fieldset[disabled] .datepicker table tr td span.active:hover,
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
.datepicker table tr td span.active.disabled:focus,
.datepicker table tr td span.active:hover.disabled:focus,
.datepicker table tr td span.active.disabled.disabled:focus,
.datepicker table tr td span.active.disabled:hover.disabled:focus,
.datepicker table tr td span.active[disabled]:focus,
.datepicker table tr td span.active:hover[disabled]:focus,
.datepicker table tr td span.active.disabled[disabled]:focus,
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
fieldset[disabled] .datepicker table tr td span.active:focus,
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
.datepicker table tr td span.active.disabled:active,
.datepicker table tr td span.active:hover.disabled:active,
.datepicker table tr td span.active.disabled.disabled:active,
.datepicker table tr td span.active.disabled:hover.disabled:active,
.datepicker table tr td span.active[disabled]:active,
.datepicker table tr td span.active:hover[disabled]:active,
.datepicker table tr td span.active.disabled[disabled]:active,
.datepicker table tr td span.active.disabled:hover[disabled]:active,
fieldset[disabled] .datepicker table tr td span.active:active,
fieldset[disabled] .datepicker table tr td span.active:hover:active,
fieldset[disabled] .datepicker table tr td span.active.disabled:active,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:active,
.datepicker table tr td span.active.disabled.active,
.datepicker table tr td span.active:hover.disabled.active,
.datepicker table tr td span.active.disabled.disabled.active,
.datepicker table tr td span.active.disabled:hover.disabled.active,
.datepicker table tr td span.active[disabled].active,
.datepicker table tr td span.active:hover[disabled].active,
.datepicker table tr td span.active.disabled[disabled].active,
.datepicker table tr td span.active.disabled:hover[disabled].active,
fieldset[disabled] .datepicker table tr td span.active.active,
fieldset[disabled] .datepicker table tr td span.active:hover.active,
fieldset[disabled] .datepicker table tr td span.active.disabled.active,
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.active {
background-color: #428bca;
border-color: #357ebd;
}
.datepicker table tr td span.old,
.datepicker table tr td span.new {
color: #999999;
}
.datepicker th.datepicker-switch {
width: 145px;
}
.datepicker thead tr:first-child th,
.datepicker tfoot tr th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover,
.datepicker tfoot tr th:hover {
background: #eeeeee;
}
.datepicker .cw {
font-size: 10px;
width: 12px;
padding: 0 2px 0 5px;
vertical-align: middle;
}
.datepicker thead tr:first-child th.cw {
cursor: default;
background-color: transparent;
}
.input-group.date .input-group-addon i {
cursor: pointer;
width: 16px;
height: 16px;
}
.input-daterange input {
text-align: center;
}
.input-daterange input:first-child {
border-radius: 3px 0 0 3px;
}
.input-daterange input:last-child {
border-radius: 0 3px 3px 0;
}
.input-daterange .input-group-addon {
width: auto;
min-width: 16px;
padding: 4px 5px;
font-weight: normal;
line-height: 1.428571429;
text-align: center;
text-shadow: 0 1px 0 #fff;
vertical-align: middle;
background-color: #eeeeee;
border-width: 1px 0;
margin-left: -5px;
margin-right: -5px;
}
.datepicker.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
float: left;
display: none;
min-width: 160px;
list-style: none;
background-color: #ffffff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 5px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
*border-right-width: 2px;
*border-bottom-width: 2px;
color: #333333;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px;
line-height: 1.428571429;
}
.datepicker.dropdown-menu th,
.datepicker.dropdown-menu td {
padding: 4px 5px;
}

View File

@@ -0,0 +1,155 @@
/* The MIT License */
.dropzone,
.dropzone *,
.dropzone-previews,
.dropzone-previews * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.dropzone {
position: relative;
border: 1px solid rgba(0,0,0,0.08);
background: rgba(0,0,0,0.02);
padding: 1em;
}
.dropzone.dz-clickable {
cursor: pointer;
}
.dropzone.dz-clickable .dz-message,
.dropzone.dz-clickable .dz-message span {
cursor: pointer;
}
.dropzone.dz-clickable * {
cursor: default;
}
.dropzone .dz-message {
opacity: 1;
-ms-filter: none;
filter: none;
}
.dropzone.dz-drag-hover {
border-color: rgba(0,0,0,0.15);
background: rgba(0,0,0,0.04);
}
.dropzone.dz-started .dz-message {
display: none;
}
.dropzone .dz-preview,
.dropzone-previews .dz-preview {
background: rgba(255,255,255,0.8);
position: relative;
display: inline-block;
margin: 17px;
vertical-align: top;
border: 1px solid #acacac;
padding: 6px 6px 6px 6px;
}
.dropzone .dz-preview.dz-file-preview [data-dz-thumbnail],
.dropzone-previews .dz-preview.dz-file-preview [data-dz-thumbnail] {
display: none;
}
.dropzone .dz-preview .dz-details,
.dropzone-previews .dz-preview .dz-details {
width: 100px;
height: 100px;
position: relative;
background: #ebebeb;
padding: 5px;
margin-bottom: 22px;
}
.dropzone .dz-preview .dz-details .dz-filename,
.dropzone-previews .dz-preview .dz-details .dz-filename {
overflow: hidden;
height: 100%;
}
.dropzone .dz-preview .dz-details img,
.dropzone-previews .dz-preview .dz-details img {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
}
.dropzone .dz-preview .dz-details .dz-size,
.dropzone-previews .dz-preview .dz-details .dz-size {
position: absolute;
bottom: -28px;
left: 3px;
height: 28px;
line-height: 28px;
}
.dropzone .dz-preview.dz-error .dz-error-mark,
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
display: block;
}
.dropzone .dz-preview.dz-success .dz-success-mark,
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
display: block;
}
.dropzone .dz-preview:hover .dz-details img,
.dropzone-previews .dz-preview:hover .dz-details img {
display: none;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark,
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark {
display: none;
position: absolute;
width: 40px;
height: 40px;
font-size: 30px;
text-align: center;
right: -10px;
top: -10px;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark {
color: #8cc657;
}
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark {
color: #ee162d;
}
.dropzone .dz-preview .dz-progress,
.dropzone-previews .dz-preview .dz-progress {
position: absolute;
top: 100px;
left: 6px;
right: 6px;
height: 6px;
background: #d7d7d7;
display: none;
}
.dropzone .dz-preview .dz-progress .dz-upload,
.dropzone-previews .dz-preview .dz-progress .dz-upload {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 0%;
background-color: #8cc657;
}
.dropzone .dz-preview.dz-processing .dz-progress,
.dropzone-previews .dz-preview.dz-processing .dz-progress {
display: block;
}
.dropzone .dz-preview .dz-error-message,
.dropzone-previews .dz-preview .dz-error-message {
display: none;
position: absolute;
top: -5px;
left: -20px;
background: rgba(245,245,245,0.8);
padding: 8px 10px;
color: #800;
min-width: 140px;
max-width: 500px;
z-index: 500;
}
.dropzone .dz-preview:hover.dz-error .dz-error-message,
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
display: block;
}

View File

@@ -0,0 +1,410 @@
/* The MIT License */
.dropzone,
.dropzone *,
.dropzone-previews,
.dropzone-previews * {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.dropzone {
position: relative;
border: 1px solid rgba(0,0,0,0.08);
background: rgba(0,0,0,0.02);
padding: 1em;
}
.dropzone.dz-clickable {
cursor: pointer;
}
.dropzone.dz-clickable .dz-message,
.dropzone.dz-clickable .dz-message span {
cursor: pointer;
}
.dropzone.dz-clickable * {
cursor: default;
}
.dropzone .dz-message {
opacity: 1;
-ms-filter: none;
filter: none;
}
.dropzone.dz-drag-hover {
border-color: rgba(0,0,0,0.15);
background: rgba(0,0,0,0.04);
}
.dropzone.dz-started .dz-message {
display: none;
}
.dropzone .dz-preview,
.dropzone-previews .dz-preview {
background: rgba(255,255,255,0.8);
position: relative;
display: inline-block;
margin: 17px;
vertical-align: top;
border: 1px solid #acacac;
padding: 6px 6px 6px 6px;
}
.dropzone .dz-preview.dz-file-preview [data-dz-thumbnail],
.dropzone-previews .dz-preview.dz-file-preview [data-dz-thumbnail] {
display: none;
}
.dropzone .dz-preview .dz-details,
.dropzone-previews .dz-preview .dz-details {
width: 100px;
height: 100px;
position: relative;
background: #ebebeb;
padding: 5px;
margin-bottom: 22px;
}
.dropzone .dz-preview .dz-details .dz-filename,
.dropzone-previews .dz-preview .dz-details .dz-filename {
overflow: hidden;
height: 100%;
}
.dropzone .dz-preview .dz-details img,
.dropzone-previews .dz-preview .dz-details img {
position: absolute;
top: 0;
left: 0;
width: 100px;
height: 100px;
}
.dropzone .dz-preview .dz-details .dz-size,
.dropzone-previews .dz-preview .dz-details .dz-size {
position: absolute;
bottom: -28px;
left: 3px;
height: 28px;
line-height: 28px;
}
.dropzone .dz-preview.dz-error .dz-error-mark,
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
display: block;
}
.dropzone .dz-preview.dz-success .dz-success-mark,
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
display: block;
}
.dropzone .dz-preview:hover .dz-details img,
.dropzone-previews .dz-preview:hover .dz-details img {
display: none;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark,
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark {
display: none;
position: absolute;
width: 40px;
height: 40px;
font-size: 30px;
text-align: center;
right: -10px;
top: -10px;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark {
color: #8cc657;
}
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark {
color: #ee162d;
}
.dropzone .dz-preview .dz-progress,
.dropzone-previews .dz-preview .dz-progress {
position: absolute;
top: 100px;
left: 6px;
right: 6px;
height: 6px;
background: #d7d7d7;
display: none;
}
.dropzone .dz-preview .dz-progress .dz-upload,
.dropzone-previews .dz-preview .dz-progress .dz-upload {
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 0%;
background-color: #8cc657;
}
.dropzone .dz-preview.dz-processing .dz-progress,
.dropzone-previews .dz-preview.dz-processing .dz-progress {
display: block;
}
.dropzone .dz-preview .dz-error-message,
.dropzone-previews .dz-preview .dz-error-message {
display: none;
position: absolute;
top: -5px;
left: -20px;
background: rgba(245,245,245,0.8);
padding: 8px 10px;
color: #800;
min-width: 140px;
max-width: 500px;
z-index: 500;
}
.dropzone .dz-preview:hover.dz-error .dz-error-message,
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
display: block;
}
.dropzone {
border: 1px solid rgba(0,0,0,0.03);
min-height: 360px;
-webkit-border-radius: 3px;
border-radius: 3px;
background: rgba(0,0,0,0.03);
padding: 23px;
}
.dropzone .dz-default.dz-message {
opacity: 1;
-ms-filter: none;
filter: none;
-webkit-transition: opacity 0.3s ease-in-out;
-moz-transition: opacity 0.3s ease-in-out;
-o-transition: opacity 0.3s ease-in-out;
-ms-transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out;
background-image: url("../images/spritemap.png");
background-repeat: no-repeat;
background-position: 0 0;
position: absolute;
width: 428px;
height: 123px;
margin-left: -214px;
margin-top: -61.5px;
top: 50%;
left: 50%;
}
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
.dropzone .dz-default.dz-message {
background-image: url("../images/spritemap@2x.png");
-webkit-background-size: 428px 406px;
-moz-background-size: 428px 406px;
background-size: 428px 406px;
}
}
.dropzone .dz-default.dz-message span {
display: none;
}
.dropzone.dz-square .dz-default.dz-message {
background-position: 0 -123px;
width: 268px;
margin-left: -134px;
height: 174px;
margin-top: -87px;
}
.dropzone.dz-drag-hover .dz-message {
opacity: 0.15;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=15)";
filter: alpha(opacity=15);
}
.dropzone.dz-started .dz-message {
display: block;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
}
.dropzone .dz-preview,
.dropzone-previews .dz-preview {
-webkit-box-shadow: 1px 1px 4px rgba(0,0,0,0.16);
box-shadow: 1px 1px 4px rgba(0,0,0,0.16);
font-size: 14px;
}
.dropzone .dz-preview.dz-image-preview:hover .dz-details img,
.dropzone-previews .dz-preview.dz-image-preview:hover .dz-details img {
display: block;
opacity: 0.1;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=10)";
filter: alpha(opacity=10);
}
.dropzone .dz-preview.dz-success .dz-success-mark,
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
opacity: 1;
-ms-filter: none;
filter: none;
}
.dropzone .dz-preview.dz-error .dz-error-mark,
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
opacity: 1;
-ms-filter: none;
filter: none;
}
.dropzone .dz-preview.dz-error .dz-progress .dz-upload,
.dropzone-previews .dz-preview.dz-error .dz-progress .dz-upload {
background: #ee1e2d;
}
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark,
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark {
display: block;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
-webkit-transition: opacity 0.4s ease-in-out;
-moz-transition: opacity 0.4s ease-in-out;
-o-transition: opacity 0.4s ease-in-out;
-ms-transition: opacity 0.4s ease-in-out;
transition: opacity 0.4s ease-in-out;
background-image: url("../images/spritemap.png");
background-repeat: no-repeat;
}
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark,
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark {
background-image: url("../images/spritemap@2x.png");
-webkit-background-size: 428px 406px;
-moz-background-size: 428px 406px;
background-size: 428px 406px;
}
}
.dropzone .dz-preview .dz-error-mark span,
.dropzone-previews .dz-preview .dz-error-mark span,
.dropzone .dz-preview .dz-success-mark span,
.dropzone-previews .dz-preview .dz-success-mark span {
display: none;
}
.dropzone .dz-preview .dz-error-mark,
.dropzone-previews .dz-preview .dz-error-mark {
background-position: -268px -123px;
}
.dropzone .dz-preview .dz-success-mark,
.dropzone-previews .dz-preview .dz-success-mark {
background-position: -268px -163px;
}
.dropzone .dz-preview .dz-progress .dz-upload,
.dropzone-previews .dz-preview .dz-progress .dz-upload {
-webkit-animation: loading 0.4s linear infinite;
-moz-animation: loading 0.4s linear infinite;
-o-animation: loading 0.4s linear infinite;
-ms-animation: loading 0.4s linear infinite;
animation: loading 0.4s linear infinite;
-webkit-transition: width 0.3s ease-in-out;
-moz-transition: width 0.3s ease-in-out;
-o-transition: width 0.3s ease-in-out;
-ms-transition: width 0.3s ease-in-out;
transition: width 0.3s ease-in-out;
-webkit-border-radius: 2px;
border-radius: 2px;
position: absolute;
top: 0;
left: 0;
width: 0%;
height: 100%;
background-image: url("../images/spritemap.png");
background-repeat: repeat-x;
background-position: 0px -400px;
}
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
.dropzone .dz-preview .dz-progress .dz-upload,
.dropzone-previews .dz-preview .dz-progress .dz-upload {
background-image: url("../images/spritemap@2x.png");
-webkit-background-size: 428px 406px;
-moz-background-size: 428px 406px;
background-size: 428px 406px;
}
}
.dropzone .dz-preview.dz-success .dz-progress,
.dropzone-previews .dz-preview.dz-success .dz-progress {
display: block;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
-webkit-transition: opacity 0.4s ease-in-out;
-moz-transition: opacity 0.4s ease-in-out;
-o-transition: opacity 0.4s ease-in-out;
-ms-transition: opacity 0.4s ease-in-out;
transition: opacity 0.4s ease-in-out;
}
.dropzone .dz-preview .dz-error-message,
.dropzone-previews .dz-preview .dz-error-message {
display: block;
opacity: 0;
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
filter: alpha(opacity=0);
-webkit-transition: opacity 0.3s ease-in-out;
-moz-transition: opacity 0.3s ease-in-out;
-o-transition: opacity 0.3s ease-in-out;
-ms-transition: opacity 0.3s ease-in-out;
transition: opacity 0.3s ease-in-out;
}
.dropzone .dz-preview:hover.dz-error .dz-error-message,
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
opacity: 1;
-ms-filter: none;
filter: none;
}
.dropzone a.dz-remove,
.dropzone-previews a.dz-remove {
background-image: -webkit-linear-gradient(top, #fafafa, #eee);
background-image: -moz-linear-gradient(top, #fafafa, #eee);
background-image: -o-linear-gradient(top, #fafafa, #eee);
background-image: -ms-linear-gradient(top, #fafafa, #eee);
background-image: linear-gradient(to bottom, #fafafa, #eee);
-webkit-border-radius: 2px;
border-radius: 2px;
border: 1px solid #eee;
text-decoration: none;
display: block;
padding: 4px 5px;
text-align: center;
color: #aaa;
margin-top: 26px;
}
.dropzone a.dz-remove:hover,
.dropzone-previews a.dz-remove:hover {
color: #666;
}
@-moz-keyframes loading {
0% {
background-position: 0 -400px;
}
100% {
background-position: -7px -400px;
}
}
@-webkit-keyframes loading {
0% {
background-position: 0 -400px;
}
100% {
background-position: -7px -400px;
}
}
@-o-keyframes loading {
0% {
background-position: 0 -400px;
}
100% {
background-position: -7px -400px;
}
}
@-ms-keyframes loading {
0% {
background-position: 0 -400px;
}
100% {
background-position: -7px -400px;
}
}
@keyframes loading {
0% {
background-position: 0 -400px;
}
100% {
background-position: -7px -400px;
}
}

View File

@@ -0,0 +1,977 @@
/*!
* FullCalendar v2.2.0 Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw
*/
.fc {
direction: ltr;
text-align: left;
}
.fc-rtl {
text-align: right;
}
body .fc { /* extra precedence to overcome jqui */
font-size: 1em;
}
/* Colors
--------------------------------------------------------------------------------------------------*/
.fc-unthemed th,
.fc-unthemed td,
.fc-unthemed hr,
.fc-unthemed thead,
.fc-unthemed tbody,
.fc-unthemed .fc-row,
.fc-unthemed .fc-popover {
border-color: #ddd;
}
.fc-unthemed .fc-popover {
background-color: #fff;
}
.fc-unthemed hr,
.fc-unthemed .fc-popover .fc-header {
background: #eee;
}
.fc-unthemed .fc-popover .fc-header .fc-close {
color: #666;
}
.fc-unthemed .fc-today {
background: #fcf8e3;
}
.fc-highlight { /* when user is selecting cells */
background: #bce8f1;
opacity: .3;
filter: alpha(opacity=30); /* for IE */
}
.fc-bgevent { /* default look for background events */
background: rgb(143, 223, 130);
opacity: .3;
filter: alpha(opacity=30); /* for IE */
}
.fc-nonbusiness { /* default look for non-business-hours areas */
/* will inherit .fc-bgevent's styles */
background: #ccc;
}
/* Icons (inline elements with styled text that mock arrow icons)
--------------------------------------------------------------------------------------------------*/
.fc-icon {
display: inline-block;
font-size: 2em;
line-height: .5em;
height: .5em; /* will make the total height 1em */
font-family: "Courier New", Courier, monospace;
}
.fc-icon-left-single-arrow:after {
content: "\02039";
font-weight: bold;
}
.fc-icon-right-single-arrow:after {
content: "\0203A";
font-weight: bold;
}
.fc-icon-left-double-arrow:after {
content: "\000AB";
}
.fc-icon-right-double-arrow:after {
content: "\000BB";
}
.fc-icon-x:after {
content: "\000D7";
}
/* Buttons (styled <button> tags, normalized to work cross-browser)
--------------------------------------------------------------------------------------------------*/
.fc button {
/* force height to include the border and padding */
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
/* dimensions */
margin: 0;
height: 2.1em;
padding: 0 .6em;
/* text & cursor */
font-size: 1em; /* normalize */
white-space: nowrap;
cursor: pointer;
}
/* Firefox has an annoying inner border */
.fc button::-moz-focus-inner { margin: 0; padding: 0; }
.fc-state-default { /* non-theme */
border: 1px solid;
}
.fc-state-default.fc-corner-left { /* non-theme */
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.fc-state-default.fc-corner-right { /* non-theme */
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
/* icons in buttons */
.fc button .fc-icon { /* non-theme */
position: relative;
top: .05em; /* seems to be a good adjustment across browsers */
margin: 0 .1em;
}
/*
button states
borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
*/
.fc-state-default {
background-color: #f5f5f5;
background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
background-repeat: repeat-x;
border-color: #e6e6e6 #e6e6e6 #bfbfbf;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
color: #333;
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.fc-state-hover,
.fc-state-down,
.fc-state-active,
.fc-state-disabled {
color: #333333;
background-color: #e6e6e6;
}
.fc-state-hover {
color: #333333;
text-decoration: none;
background-position: 0 -15px;
-webkit-transition: background-position 0.1s linear;
-moz-transition: background-position 0.1s linear;
-o-transition: background-position 0.1s linear;
transition: background-position 0.1s linear;
}
.fc-state-down,
.fc-state-active {
background-color: #cccccc;
background-image: none;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.fc-state-disabled {
cursor: default;
background-image: none;
opacity: 0.65;
filter: alpha(opacity=65);
box-shadow: none;
}
/* Buttons Groups
--------------------------------------------------------------------------------------------------*/
.fc-button-group {
display: inline-block;
}
/*
every button that is not first in a button group should scootch over one pixel and cover the
previous button's border...
*/
.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */
float: left;
margin: 0 0 0 -1px;
}
.fc .fc-button-group > :first-child { /* same */
margin-left: 0;
}
/* Popover
--------------------------------------------------------------------------------------------------*/
.fc-popover {
position: absolute;
box-shadow: 0 2px 6px rgba(0,0,0,.15);
}
.fc-popover .fc-header {
padding: 2px 4px;
}
.fc-popover .fc-header .fc-title {
margin: 0 2px;
}
.fc-popover .fc-header .fc-close {
cursor: pointer;
}
.fc-ltr .fc-popover .fc-header .fc-title,
.fc-rtl .fc-popover .fc-header .fc-close {
float: left;
}
.fc-rtl .fc-popover .fc-header .fc-title,
.fc-ltr .fc-popover .fc-header .fc-close {
float: right;
}
/* unthemed */
.fc-unthemed .fc-popover {
border-width: 1px;
border-style: solid;
}
.fc-unthemed .fc-popover .fc-header .fc-close {
font-size: 25px;
margin-top: 4px;
}
/* jqui themed */
.fc-popover > .ui-widget-header + .ui-widget-content {
border-top: 0; /* where they meet, let the header have the border */
}
/* Misc Reusable Components
--------------------------------------------------------------------------------------------------*/
.fc hr {
height: 0;
margin: 0;
padding: 0 0 2px; /* height is unreliable across browsers, so use padding */
border-style: solid;
border-width: 1px 0;
}
.fc-clear {
clear: both;
}
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-helper-skeleton {
/* these element should always cling to top-left/right corners */
position: absolute;
top: 0;
left: 0;
right: 0;
}
.fc-bg {
bottom: 0; /* strech bg to bottom edge */
}
.fc-bg table {
height: 100%; /* strech bg to bottom edge */
}
/* Tables
--------------------------------------------------------------------------------------------------*/
.fc table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border-spacing: 0;
font-size: 1em; /* normalize cross-browser */
}
.fc th {
text-align: center;
}
.fc th,
.fc td {
border-style: solid;
border-width: 1px;
padding: 0;
vertical-align: top;
}
.fc td.fc-today {
border-style: double; /* overcome neighboring borders */
}
/* Fake Table Rows
--------------------------------------------------------------------------------------------------*/
.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */
/* no visible border by default. but make available if need be (scrollbar width compensation) */
border-style: solid;
border-width: 0;
}
.fc-row table {
/* don't put left/right border on anything within a fake row.
the outer tbody will worry about this */
border-left: 0 hidden transparent;
border-right: 0 hidden transparent;
/* no bottom borders on rows */
border-bottom: 0 hidden transparent;
}
.fc-row:first-child table {
border-top: 0 hidden transparent; /* no top border on first row */
}
/* Day Row (used within the header and the DayGrid)
--------------------------------------------------------------------------------------------------*/
.fc-row {
position: relative;
}
.fc-row .fc-bg {
z-index: 1;
}
/* highlighting cells & background event skeleton */
.fc-row .fc-bgevent-skeleton,
.fc-row .fc-highlight-skeleton {
bottom: 0; /* stretch skeleton to bottom of row */
}
.fc-row .fc-bgevent-skeleton table,
.fc-row .fc-highlight-skeleton table {
height: 100%; /* stretch skeleton to bottom of row */
}
.fc-row .fc-highlight-skeleton td,
.fc-row .fc-bgevent-skeleton td {
border-color: transparent;
}
.fc-row .fc-bgevent-skeleton {
z-index: 2;
}
.fc-row .fc-highlight-skeleton {
z-index: 3;
}
/*
row content (which contains day/week numbers and events) as well as "helper" (which contains
temporary rendered events).
*/
.fc-row .fc-content-skeleton {
position: relative;
z-index: 4;
padding-bottom: 2px; /* matches the space above the events */
}
.fc-row .fc-helper-skeleton {
z-index: 5;
}
.fc-row .fc-content-skeleton td,
.fc-row .fc-helper-skeleton td {
/* see-through to the background below */
background: none; /* in case <td>s are globally styled */
border-color: transparent;
/* don't put a border between events and/or the day number */
border-bottom: 0;
}
.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */
.fc-row .fc-helper-skeleton tbody td {
/* don't put a border between event cells */
border-top: 0;
}
/* Scrolling Container
--------------------------------------------------------------------------------------------------*/
.fc-scroller { /* this class goes on elements for guaranteed vertical scrollbars */
overflow-y: scroll;
overflow-x: hidden;
}
.fc-scroller > * { /* we expect an immediate inner element */
position: relative; /* re-scope all positions */
width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */
overflow: hidden; /* don't let negative margins or absolute positioning create further scroll */
}
/* Global Event Styles
--------------------------------------------------------------------------------------------------*/
.fc-event {
position: relative; /* for resize handle and other inner positioning */
display: block; /* make the <a> tag block */
font-size: .85em;
line-height: 1.3;
border-radius: 3px;
border: 1px solid #3a87ad; /* default BORDER color */
background-color: #3a87ad; /* default BACKGROUND color */
font-weight: normal; /* undo jqui's ui-widget-header bold */
}
/* overpower some of bootstrap's and jqui's styles on <a> tags */
.fc-event,
.fc-event:hover,
.ui-widget .fc-event {
color: #fff; /* default TEXT color */
text-decoration: none; /* if <a> has an href */
}
.fc-event[href],
.fc-event.fc-draggable {
cursor: pointer; /* give events with links and draggable events a hand mouse pointer */
}
.fc-not-allowed, /* causes a "warning" cursor. applied on body */
.fc-not-allowed .fc-event { /* to override an event's custom cursor */
cursor: not-allowed;
}
/* DayGrid events
----------------------------------------------------------------------------------------------------
We use the full "fc-day-grid-event" class instead of using descendants because the event won't
be a descendant of the grid when it is being dragged.
*/
.fc-day-grid-event {
margin: 1px 2px 0; /* spacing between events and edges */
padding: 0 1px;
}
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */
.fc-ltr .fc-day-grid-event.fc-not-start,
.fc-rtl .fc-day-grid-event.fc-not-end {
margin-left: 0;
border-left-width: 0;
padding-left: 1px; /* replace the border with padding */
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.fc-ltr .fc-day-grid-event.fc-not-end,
.fc-rtl .fc-day-grid-event.fc-not-start {
margin-right: 0;
border-right-width: 0;
padding-right: 1px; /* replace the border with padding */
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.fc-day-grid-event > .fc-content { /* force events to be one-line tall */
white-space: nowrap;
overflow: hidden;
}
.fc-day-grid-event .fc-time {
font-weight: bold;
}
/* resize handle (outside of fc-content, so can go outside of bounds) */
.fc-day-grid-event .fc-resizer {
position: absolute;
top: 0;
bottom: 0;
width: 7px;
}
.fc-ltr .fc-day-grid-event .fc-resizer {
right: -3px;
cursor: e-resize;
}
.fc-rtl .fc-day-grid-event .fc-resizer {
left: -3px;
cursor: w-resize;
}
/* Event Limiting
--------------------------------------------------------------------------------------------------*/
/* "more" link that represents hidden events */
a.fc-more {
margin: 1px 3px;
font-size: .85em;
cursor: pointer;
text-decoration: none;
}
a.fc-more:hover {
text-decoration: underline;
}
.fc-limited { /* rows and cells that are hidden because of a "more" link */
display: none;
}
/* popover that appears when "more" link is clicked */
.fc-day-grid .fc-row {
z-index: 1; /* make the "more" popover one higher than this */
}
.fc-more-popover {
z-index: 2;
width: 220px;
}
.fc-more-popover .fc-event-container {
padding: 10px;
}
/* Toolbar
--------------------------------------------------------------------------------------------------*/
.fc-toolbar {
text-align: center;
margin-bottom: 1em;
}
.fc-toolbar .fc-left {
float: left;
}
.fc-toolbar .fc-right {
float: right;
}
.fc-toolbar .fc-center {
display: inline-block;
}
/* the things within each left/right/center section */
.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */
float: left;
margin-left: .75em;
}
/* the first thing within each left/center/right section */
.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */
margin-left: 0;
}
/* title text */
.fc-toolbar h2 {
margin: 0;
}
/* button layering (for border precedence) */
.fc-toolbar button {
position: relative;
}
.fc-toolbar .fc-state-hover,
.fc-toolbar .ui-state-hover {
z-index: 2;
}
.fc-toolbar .fc-state-down {
z-index: 3;
}
.fc-toolbar .fc-state-active,
.fc-toolbar .ui-state-active {
z-index: 4;
}
.fc-toolbar button:focus {
z-index: 5;
}
/* View Structure
--------------------------------------------------------------------------------------------------*/
/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */
/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */
.fc-view-container *,
.fc-view-container *:before,
.fc-view-container *:after {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.fc-view, /* scope positioning and z-index's for everything within the view */
.fc-view > table { /* so dragged elements can be above the view's main element */
position: relative;
z-index: 1;
}
/* BasicView
--------------------------------------------------------------------------------------------------*/
/* day row structure */
.fc-basicWeek-view .fc-content-skeleton,
.fc-basicDay-view .fc-content-skeleton {
/* we are sure there are no day numbers in these views, so... */
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */
}
.fc-basic-view tbody .fc-row {
min-height: 4em; /* ensure that all rows are at least this tall */
}
/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */
.fc-row.fc-rigid {
overflow: hidden;
}
.fc-row.fc-rigid .fc-content-skeleton {
position: absolute;
top: 0;
left: 0;
right: 0;
}
/* week and day number styling */
.fc-basic-view .fc-week-number,
.fc-basic-view .fc-day-number {
padding: 0 2px;
}
.fc-basic-view td.fc-week-number span,
.fc-basic-view td.fc-day-number {
padding-top: 2px;
padding-bottom: 2px;
}
.fc-basic-view .fc-week-number {
text-align: center;
}
.fc-basic-view .fc-week-number span {
/* work around the way we do column resizing and ensure a minimum width */
display: inline-block;
min-width: 1.25em;
}
.fc-ltr .fc-basic-view .fc-day-number {
text-align: right;
}
.fc-rtl .fc-basic-view .fc-day-number {
text-align: left;
}
.fc-day-number.fc-other-month {
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
/* opacity with small font can sometimes look too faded
might want to set the 'color' property instead
making day-numbers bold also fixes the problem */
}
/* AgendaView all-day area
--------------------------------------------------------------------------------------------------*/
.fc-agenda-view .fc-day-grid {
position: relative;
z-index: 2; /* so the "more.." popover will be over the time grid */
}
.fc-agenda-view .fc-day-grid .fc-row {
min-height: 3em; /* all-day section will never get shorter than this */
}
.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton {
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
padding-bottom: 1em; /* give space underneath events for clicking/selecting days */
}
/* TimeGrid axis running down the side (for both the all-day area and the slot area)
--------------------------------------------------------------------------------------------------*/
.fc .fc-axis { /* .fc to overcome default cell styles */
vertical-align: middle;
padding: 0 4px;
white-space: nowrap;
}
.fc-ltr .fc-axis {
text-align: right;
}
.fc-rtl .fc-axis {
text-align: left;
}
.ui-widget td.fc-axis {
font-weight: normal; /* overcome jqui theme making it bold */
}
/* TimeGrid Structure
--------------------------------------------------------------------------------------------------*/
.fc-time-grid-container, /* so scroll container's z-index is below all-day */
.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */
position: relative;
z-index: 1;
}
.fc-time-grid {
min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */
}
.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */
border: 0 hidden transparent;
}
.fc-time-grid > .fc-bg {
z-index: 1;
}
.fc-time-grid .fc-slats,
.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */
position: relative;
z-index: 2;
}
.fc-time-grid .fc-bgevent-skeleton,
.fc-time-grid .fc-content-skeleton {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.fc-time-grid .fc-bgevent-skeleton {
z-index: 3;
}
.fc-time-grid .fc-highlight-skeleton {
z-index: 4;
}
.fc-time-grid .fc-content-skeleton {
z-index: 5;
}
.fc-time-grid .fc-helper-skeleton {
z-index: 6;
}
/* TimeGrid Slats (lines that run horizontally)
--------------------------------------------------------------------------------------------------*/
.fc-slats td {
height: 1.5em;
border-bottom: 0; /* each cell is responsible for its top border */
}
.fc-slats .fc-minor td {
border-top-style: dotted;
}
.fc-slats .ui-widget-content { /* for jqui theme */
background: none; /* see through to fc-bg */
}
/* TimeGrid Highlighting Slots
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */
position: relative; /* scopes the left/right of the fc-highlight to be in the column */
}
.fc-time-grid .fc-highlight {
position: absolute;
left: 0;
right: 0;
/* top and bottom will be in by JS */
}
/* TimeGrid Event Containment
--------------------------------------------------------------------------------------------------*/
.fc-time-grid .fc-event-container, /* a div within a cell within the fc-content-skeleton */
.fc-time-grid .fc-bgevent-container { /* a div within a cell within the fc-bgevent-skeleton */
position: relative;
}
.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */
margin: 0 2.5% 0 2px;
}
.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */
margin: 0 2px 0 2.5%;
}
.fc-time-grid .fc-event,
.fc-time-grid .fc-bgevent {
position: absolute;
z-index: 1; /* scope inner z-index's */
}
.fc-time-grid .fc-bgevent {
/* background events always span full width */
left: 0;
right: 0;
}
/* TimeGrid Event Styling
----------------------------------------------------------------------------------------------------
We use the full "fc-time-grid-event" class instead of using descendants because the event won't
be a descendant of the grid when it is being dragged.
*/
.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */
/* replace space made by the top border with padding */
border-top-width: 0;
padding-top: 1px;
/* remove top rounded corners */
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.fc-time-grid-event.fc-not-end {
/* replace space made by the top border with padding */
border-bottom-width: 0;
padding-bottom: 1px;
/* remove bottom rounded corners */
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.fc-time-grid-event {
overflow: hidden; /* don't let the bg flow over rounded corners */
}
.fc-time-grid-event > .fc-content { /* contains the time and title, but no bg and resizer */
position: relative;
z-index: 2; /* above the bg */
}
.fc-time-grid-event .fc-time,
.fc-time-grid-event .fc-title {
padding: 0 1px;
}
.fc-time-grid-event .fc-time {
font-size: .85em;
white-space: nowrap;
}
.fc-time-grid-event .fc-bg {
z-index: 1;
background: #fff;
opacity: .25;
filter: alpha(opacity=25); /* for IE */
}
/* short mode, where time and title are on the same line */
.fc-time-grid-event.fc-short .fc-content {
/* don't wrap to second line (now that contents will be inline) */
white-space: nowrap;
}
.fc-time-grid-event.fc-short .fc-time,
.fc-time-grid-event.fc-short .fc-title {
/* put the time and title on the same line */
display: inline-block;
vertical-align: top;
}
.fc-time-grid-event.fc-short .fc-time span {
display: none; /* don't display the full time text... */
}
.fc-time-grid-event.fc-short .fc-time:before {
content: attr(data-start); /* ...instead, display only the start time */
}
.fc-time-grid-event.fc-short .fc-time:after {
content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */
}
.fc-time-grid-event.fc-short .fc-title {
font-size: .85em; /* make the title text the same size as the time */
padding: 0; /* undo padding from above */
}
/* resizer */
.fc-time-grid-event .fc-resizer {
position: absolute;
z-index: 3; /* above content */
left: 0;
right: 0;
bottom: 0;
height: 8px;
overflow: hidden;
line-height: 8px;
font-size: 11px;
font-family: monospace;
text-align: center;
cursor: s-resize;
}
.fc-time-grid-event .fc-resizer:after {
content: "=";
}

View File

@@ -0,0 +1,202 @@
/*!
* FullCalendar v2.2.0 Print Stylesheet
* Docs & License: http://arshaw.com/fullcalendar/
* (c) 2013 Adam Shaw
*/
/*
* Include this stylesheet on your page to get a more printer-friendly calendar.
* When including this stylesheet, use the media='print' attribute of the <link> tag.
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
*/
.fc {
max-width: 100% !important;
}
/* Global Event Restyling
--------------------------------------------------------------------------------------------------*/
.fc-event {
background: #fff !important;
color: #000 !important;
page-break-inside: avoid;
}
.fc-event .fc-resizer {
display: none;
}
/* Table & Day-Row Restyling
--------------------------------------------------------------------------------------------------*/
th,
td,
hr,
thead,
tbody,
.fc-row {
border-color: #ccc !important;
background: #fff !important;
}
/* kill the overlaid, absolutely-positioned common components */
.fc-bg,
.fc-bgevent-skeleton,
.fc-highlight-skeleton,
.fc-helper-skeleton {
display: none;
}
/* don't force a min-height on rows (for DayGrid) */
.fc tbody .fc-row {
height: auto !important; /* undo height that JS set in distributeHeight */
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
}
.fc tbody .fc-row .fc-content-skeleton {
position: static; /* undo .fc-rigid */
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
}
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
}
.fc tbody .fc-row .fc-content-skeleton table {
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
making it look more like 3em. for other browers, it will already be this tall */
height: 1em;
}
/* Undo month-view event limiting. Display all events and hide the "more" links
--------------------------------------------------------------------------------------------------*/
.fc-more-cell,
.fc-more {
display: none !important;
}
.fc tr.fc-limited {
display: table-row !important;
}
.fc td.fc-limited {
display: table-cell !important;
}
.fc-popover {
display: none; /* never display the "more.." popover in print mode */
}
/* TimeGrid Restyling
--------------------------------------------------------------------------------------------------*/
/* undo the min-height 100% trick used to fill the container's height */
.fc-time-grid {
min-height: 0 !important;
}
/* don't display the side axis at all ("all-day" and time cells) */
.fc-agenda-view .fc-axis {
display: none;
}
/* don't display the horizontal lines */
.fc-slats,
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
display: none !important; /* important overrides inline declaration */
}
/* let the container that holds the events be naturally positioned and create real height */
.fc-time-grid .fc-content-skeleton {
position: static;
}
/* in case there are no events, we still want some height */
.fc-time-grid .fc-content-skeleton table {
height: 4em;
}
/* kill the horizontal spacing made by the event container. event margins will be done below */
.fc-time-grid .fc-event-container {
margin: 0 !important;
}
/* TimeGrid *Event* Restyling
--------------------------------------------------------------------------------------------------*/
/* naturally position events, vertically stacking them */
.fc-time-grid .fc-event {
position: static !important;
margin: 3px 2px !important;
}
/* for events that continue to a future day, give the bottom border back */
.fc-time-grid .fc-event.fc-not-end {
border-bottom-width: 1px !important;
}
/* indicate the event continues via "..." text */
.fc-time-grid .fc-event.fc-not-end:after {
content: "...";
}
/* for events that are continuations from previous days, give the top border back */
.fc-time-grid .fc-event.fc-not-start {
border-top-width: 1px !important;
}
/* indicate the event is a continuation via "..." text */
.fc-time-grid .fc-event.fc-not-start:before {
content: "...";
}
/* time */
/* undo a previous declaration and let the time text span to a second line */
.fc-time-grid .fc-event .fc-time {
white-space: normal !important;
}
/* hide the the time that is normally displayed... */
.fc-time-grid .fc-event .fc-time span {
display: none;
}
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
.fc-time-grid .fc-event .fc-time:after {
content: attr(data-full);
}
/* Vertical Scroller & Containers
--------------------------------------------------------------------------------------------------*/
/* kill the scrollbars and allow natural height */
.fc-scroller,
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
.fc-time-grid-container { /* */
overflow: visible !important;
height: auto !important;
}
/* kill the horizontal border/padding used to compensate for scrollbars */
.fc-row {
border: 0 !important;
margin: 0 !important;
}
/* Button Controls
--------------------------------------------------------------------------------------------------*/
.fc-button-group,
.fc button {
display: none; /* don't display any button-related controls */
}

View File

@@ -0,0 +1,59 @@
/* iCheck plugin Square skin, green
----------------------------------- */
.icheckbox_square-green,
.iradio_square-green {
display: inline-block;
*display: inline;
vertical-align: middle;
margin: 0;
padding: 0;
width: 22px;
height: 22px;
background: url(green.png) no-repeat;
border: none;
cursor: pointer;
}
.icheckbox_square-green {
background-position: 0 0;
}
.icheckbox_square-green.hover {
background-position: -24px 0;
}
.icheckbox_square-green.checked {
background-position: -48px 0;
}
.icheckbox_square-green.disabled {
background-position: -72px 0;
cursor: default;
}
.icheckbox_square-green.checked.disabled {
background-position: -96px 0;
}
.iradio_square-green {
background-position: -120px 0;
}
.iradio_square-green.hover {
background-position: -144px 0;
}
.iradio_square-green.checked {
background-position: -168px 0;
}
.iradio_square-green.disabled {
background-position: -192px 0;
cursor: default;
}
.iradio_square-green.checked.disabled {
background-position: -216px 0;
}
/* HiDPI support */
@media (-o-min-device-pixel-ratio: 5/4), (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) {
.icheckbox_square-green,
.iradio_square-green {
background-image: url(green@2x.png);
-webkit-background-size: 240px 24px;
background-size: 240px 24px;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Some files were not shown because too many files have changed in this diff Show More