提交 f1d4cae2 编写于 作者: baltery's avatar baltery

Delete 0.3.1 version code

上级 da4bd937
FROM alpine
MAINTAINER xRain <xrain@simcu.com>
RUN apk add --update openssh sshpass python py-mysqldb py-psutil py-crypto && \
rm -rf /var/cache/apk/*
COPY . /jumpserver
WORKDIR /jumpserver
RUN python /jumpserver/install/docker/get-pip.py && \
pip install -r /jumpserver/install/docker/piprequires.txt && \
rm -rf /jumpserver/docs && \
cp /jumpserver/install/docker/run.sh /run.sh && \
rm -rf /etc/motd && chmod +x /run.sh && \
rm -rf /jumpserver/keys && \
rm -rf /jumpserver/logs && \
rm -rf /home && \
rm -rf /etc/ssh && \
rm -rf /etc/shadow && \
rm -rf /etc/passwd && \
cp -r /jumpserver/install/docker/useradd /usr/sbin/useradd && \
cp -r /jumpserver/install/docker/userdel /usr/sbin/userdel && \
chmod +x /usr/sbin/useradd && \
chmod +x /usr/sbin/userdel && \
mkdir -p /data/home && \
mkdir -p /data/logs && \
mkdir -p /data/keys && \
mkdir -p /data/ssh && \
cp -r /jumpserver/install/docker/shadow /data/shadow && \
cp -r /jumpserver/install/docker/passwd /data/passwd && \
ln -s /data/logs /jumpserver/logs && \
ln -s /data/keys /jumpserver/keys && \
ln -s /data/home /home && \
ln -s /data/ssh /etc/ssh && \
ln -s /data/passwd /etc/passwd && \
ln -s /data/shadow /etc/shadow && \
chmod -R 777 /jumpserver
VOLUME /data
EXPOSE 80 22
CMD /run.sh
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.
## 写在前面
- 版本号变更 2.0 -> 0.2版本 3.0 -> 0.3版本
#欢迎使用Jumpserver
**Jumpserver** 是一款由python编写开源的跳板机(堡垒机)系统,实现了跳板机应有的功能。基于ssh协议来管理,客户端无需安装agent。
支持常见系统:
1. CentOS, RedHat, Fedora, Amazon Linux
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)
交流群: 552054376
### 团队
![](https://github.com/ibuler/static/raw/master/jumpserver3/team.jpg)
此差异已折叠。
version: '2'
services:
jumpserver:
build: .
container_name: jumpserver
restart: always
ports:
- "8888:80"
- "2222:22"
# environment:
# - ENGINE=mysql
# - MYSQL_HOST=192.168.64.5
# - MYSQL_PORT=3306
# - MYSQL_USER=root
# - MYSQL_PASS=love1314
# - MYSQL_NAME=jumpserver
# - MAIL_ENABLED=false
快速安装
------
####环境
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 测试文件的上传下载,日志审计 - 上传下载 查看上传下载记录
\ No newline at end of file
# 使用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判断的那三行)。
\ No newline at end of file
#!/bin/sh
#
trap '' SIGINT
base_dir=$(dirname $0)
export LANG='zh_CN.UTF-8'
python $base_dir/connect.py
exit
# coding: utf8
Jumpserver开发者文档
开发规范:
1. 遵守PE8规范 1) 命名规范 2) 导入模块规范 3) 空行规范 4) 长度规范
2. 缩进统一4个空格
3. 变量命名明了易懂多个单词下划线隔开
4. 注释到位
框架说明:
1. 项目名称 Jumpserver
2. APP:
juser 用户管理
jasset 资产管理(设备管理)
jpermission 授权管理
jlog 日志管理
3. connect.py 用户登录入口程序
4. logs 日志保存目录
5. jumpserver.conf 配置文件
6. docs 文档目录
7. static 静态文件目录
8. templates 模板目录
connect.py逻辑说明:
用户登录系统,运行该脚本,p调用get_user_host函数查看有权限的服务器ip
输入部分IP,verify_connect匹配该部分ip,如果是匹配到多个,就显示ip
匹配到0了就显示没有权限或者主机,
匹配到1个则继续
查询该服务器是否支持ldap 如果是,获得ldap用户密码登陆
如果否,查询授权表,查看该服务器授权的系统用户,并返回对应账号密码,登陆
connect函数是登陆函数,采用paramiko 使用channel登陆,posix_shell 来完成交互,并记录日志
signal模块来完成窗口改变导致的tty大小随之改变
PyCrypt是对称加密类
\ No newline at end of file
[base]
url =
key = 941enj9neshd1wes
ip = 0.0.0.0
port = 80
log = debug
[db]
engine = __ENGINE__
host = __MYSQL_HOST__
port = __MYSQL_PORT__
user = __MYSQL_USER__
password = __MYSQL_PASS__
database = __DATEBASE__
[mail]
mail_enable = __MAIL_ENABLED__
email_host = __MAIL_HOST__
email_port = __MAIL_PORT__
email_host_user = __MAIL_USER__
email_host_password = __MAIL_PASS__
email_use_tls = __MAIL_USE_TLS__
[connect]
nav_sort_by = ip
此差异已折叠。
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/bin/sh
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
postgres:x:70:70::/var/lib/postgresql:/bin/sh
nut:x:84:84:nut:/var/state/nut:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
\ No newline at end of file
#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
#!/bin/sh
cp -r /jumpserver/install/docker/config_tmpl.conf /jumpserver/jumpserver.conf
if [ ! -n "${USE_MYSQL}" ]; then
sed -i "s/__USE_MYSQL__/false/" /jumpserver/jumpserver.conf
else
sed -i "s/__USE_MYSQL__/true/" /jumpserver/jumpserver.conf
sed -i "s/__MYSQL_HOST__/${MYSQL_HOST}/" /jumpserver/jumpserver.conf
sed -i "s/__MYSQL_PORT__/${MYSQL_PORT}/" /jumpserver/jumpserver.conf
sed -i "s/__MYSQL_USER__/${MYSQL_USER}/" /jumpserver/jumpserver.conf
sed -i "s/__MYSQL_PASS__/${MYSQL_PASS}/" /jumpserver/jumpserver.conf
sed -i "s/__MYSQL_NAME__/${MYSQL_NAME}/" /jumpserver/jumpserver.conf
fi
if [ ! -n "${MAIL_ENABLED}" ]; then
sed -i "s/__MAIL_ENABLED__/false/" /jumpserver/jumpserver.conf
else
sed -i "s/__MAIL_ENABLED__/${MAIL_ENABLED}/" /jumpserver/jumpserver.conf
sed -i "s/__MAIL_HOST__/${MAIL_HOST}/" /jumpserver/jumpserver.conf
sed -i "s/__MAIL_PORT__/${MAIL_PORT}/" /jumpserver/jumpserver.conf
sed -i "s/__MAIL_USER__/${MAIL_USER}/" /jumpserver/jumpserver.conf
sed -i "s/__MAIL_PASS__/${MAIL_PASS}/" /jumpserver/jumpserver.conf
fi
if [ ! -n "${MAIL_USE_TLS}" ]; then
sed -i "s/__MAIL_USE_TLS__/false/" /jumpserver/jumpserver.conf
else
sed -i "s/__MAIL_USE_TLS__/${MAIL_USE_TLS}/" /jumpserver/jumpserver.conf
fi
if [ ! -f "/etc/ssh/sshd_config" ]; then
cp -r /jumpserver/install/docker/sshd_config /etc/ssh/sshd_config
fi
if [ ! -f "/etc/ssh/ssh_host_rsa_key" ]; then
ssh-keygen -t rsa -b 2048 -f /etc/ssh/ssh_host_rsa_key -N ''
fi
if [ ! -f "/etc/ssh/ssh_host_dsa_key" ]; then
ssh-keygen -t dsa -b 1024 -f /etc/ssh/ssh_host_dsa_key -N ''
fi
if [ ! -f "/etc/ssh/ssh_host_ecdsa_key" ]; then
ssh-keygen -t ecdsa -b 521 -f /etc/ssh/ssh_host_ecdsa_key -N ''
fi
if [ ! -f "/etc/ssh/ssh_host_ed25519_key" ]; then
ssh-keygen -t ed25519 -b 1024 -f /etc/ssh/ssh_host_ed25519_key -N ''
fi
/usr/sbin/sshd -E /data/logs/jumpserver.log
python /jumpserver/manage.py syncdb --noinput
if [ ! -f "/home/init.locked" ]; then
python manage.py loaddata install/initial_data.yaml
date > /home/init.locked
fi
python /jumpserver/run_server.py >> /data/logs/jumpserver.log &
chmod -R 777 /data/logs/jumpserver.log
tail -f /data/logs/jumpserver.log
root:::0:::::
bin:!::0:::::
daemon:!::0:::::
adm:!::0:::::
lp:!::0:::::
sync:!::0:::::
shutdown:!::0:::::
halt:!::0:::::
mail:!::0:::::
news:!::0:::::
uucp:!::0:::::
operator:!::0:::::
man:!::0:::::
postmaster:!::0:::::
cron:!::0:::::
ftp:!::0:::::
sshd:!::0:::::
at:!::0:::::
squid:!::0:::::
xfs:!::0:::::
games:!::0:::::
postgres:!::0:::::
nut:!::0:::::
cyrus:!::0:::::
vpopmail:!::0:::::
ntp:!::0:::::
smmsp:!::0:::::
guest:!::0:::::
nobody:!::0:::::
\ No newline at end of file
# $OpenBSD: sshd_config,v 1.98 2016/02/17 05:29:04 djm Exp $
# This is the sshd server system-wide configuration file. See
# sshd_config(5) for more information.
# This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin
# The strategy used for options in the default sshd_config shipped with
# OpenSSH is to specify options with their default value where
# possible, but leave them commented. Uncommented options override the
# default value.
#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::
# The default requires explicit activation of protocol 1
#Protocol 2
# HostKey for protocol version 1
#HostKey /etc/ssh/ssh_host_key
# HostKeys for protocol version 2
#HostKey /etc/ssh/ssh_host_rsa_key
#HostKey /etc/ssh/ssh_host_dsa_key
#HostKey /etc/ssh/ssh_host_ecdsa_key
#HostKey /etc/ssh/ssh_host_ed25519_key
# Lifetime and size of ephemeral version 1 server key
#KeyRegenerationInterval 1h
#ServerKeyBits 1024
# Ciphers and keying
#RekeyLimit default none
# Logging
# obsoletes QuietMode and FascistLogging
#SyslogFacility AUTH
#LogLevel INFO
# Authentication:
#LoginGraceTime 2m
#PermitRootLogin prohibit-password
#StrictModes yes
#MaxAuthTries 6
#MaxSessions 10
#RSAAuthentication yes
#PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
# but this is overridden so installations will only check .ssh/authorized_keys
AuthorizedKeysFile .ssh/authorized_keys
#AuthorizedPrincipalsFile none
#AuthorizedKeysCommand none
#AuthorizedKeysCommandUser nobody
# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
#RhostsRSAAuthentication no
# similar for protocol version 2
#HostbasedAuthentication no
# Change to yes if you don't trust ~/.ssh/known_hosts for
# RhostsRSAAuthentication and HostbasedAuthentication
#IgnoreUserKnownHosts no
# Don't read the user's ~/.rhosts and ~/.shosts files
#IgnoreRhosts yes
# To disable tunneled clear text passwords, change to no here!
#PasswordAuthentication yes
#PermitEmptyPasswords no
# Change to no to disable s/key passwords
#ChallengeResponseAuthentication yes
# Kerberos options
#KerberosAuthentication no
#KerberosOrLocalPasswd yes
#KerberosTicketCleanup yes
#KerberosGetAFSToken no
# GSSAPI options
#GSSAPIAuthentication no
#GSSAPICleanupCredentials yes
# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication and
# PasswordAuthentication. Depending on your PAM configuration,
# PAM authentication via ChallengeResponseAuthentication may bypass
# the setting of "PermitRootLogin without-password".
# If you just want the PAM account and session checks to run without
# PAM authentication, then enable this but set PasswordAuthentication
# and ChallengeResponseAuthentication to 'no'.
#UsePAM no
#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
#X11Forwarding no
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes
#PrintMotd yes
#PrintLastLog yes
#TCPKeepAlive yes
#UseLogin no
#UsePrivilegeSeparation sandbox
#PermitUserEnvironment no
#Compression delayed
#ClientAliveInterval 0
#ClientAliveCountMax 3
#UseDNS no
#PidFile /run/sshd.pid
#MaxStartups 10:30:100
#PermitTunnel no
#ChrootDirectory none
#VersionAddendum none
# no default banner path
#Banner none
# override default of no subsystems
Subsystem sftp /usr/lib/ssh/sftp-server
# the following are HPN related configuration options
# tcp receive buffer polling. disable in non autotuning kernels
#TcpRcvBufPoll yes
# disable hpn performance boosts
#HPNDisabled no
# buffer size for hpn to non-hpn connections
#HPNBufferSize 2048
# Example of overriding settings on a per-user basis
#Match User anoncvs
# X11Forwarding no
# AllowTcpForwarding no
# PermitTTY no
# ForceCommand cvs server
\ No newline at end of file
#!/bin/sh
adduser $@
\ No newline at end of file
#!/bin/sh
deluser --remove-home $3
\ No newline at end of file
# -*-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
- 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
#!/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.linux_distribution()[0].lower()
self.version = platform.linux_distribution()[1]
@property
def _is_redhat(self):
if self.dist.startswith("centos") or self.dist.startswith("red") or self.dist == "fedora" or self.dist == "amazon linux ami":
return True
@property
def _is_centos7(self):
if self.dist.startswith("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, Amazon Linux, 暂不支持其他平台安装.")
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')
bash('rm -rf /usr/lib64/python2.6/site-packages/Crypto/')
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()
#!/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登录请访问http://ip:8000, 祝你使用愉快。\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()
#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
from django.contrib import admin
# Register your models here.
# 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)
# 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'})
}
# 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=256, 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
from django.test import TestCase
# Create your tests here.
# 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'),
)
\ No newline at end of file
此差异已折叠。
from django.contrib import admin
# Register your models here.
# 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)
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)
from django.test import TestCase
# Create your tests here.
# 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'),
)
\ No newline at end of file
# 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(100, 35)
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)):
mkdir(os.path.join(path, 'tty', date), mode=777)
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
# Jperm App
---
### 模块 ansible_api
> 使用说明
+ 依赖rpm安装包: ansible、 sshpass
+ 依赖pip安装包: passlib
+ 关于ansible配置: 需要启用配置文件(/etc/ansible/ansible.cfg)的 host_key_checking = False
from django.contrib import admin
# Register your models here.
此差异已折叠。
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=512)
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)
此差异已折叠。
---
- hosts: 'add_users_group'
gather_facts: no
tasks:
- name: add SA user
command: uname -a
---
- hosts: test
gather_facts: no
tasks:
- name: just for test
command: uname -a
from django.test import TestCase
# Create your tests here.
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'),
)
# -*- 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=755)
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()
此差异已折叠。
[base]
url = http://127.0.0.1
key = 941enj9neshd1wes
ip = 0.0.0.0
port = 8000
log = debug
[db]
engine = mysql
host = 127.0.0.1
port = 3306
user = jumpserver
password = mysql234
database = jumpserver
[mail]
mail_enable = 1
email_host =
email_port = 587
email_host_user =
email_host_password =
email_use_tls = False
email_use_ssl = False
[connect]
nav_sort_by = ip
此差异已折叠。
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
# 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=256, 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
此差异已折叠。
# -*- 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
此差异已折叠。
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')),
)
此差异已折叠。
"""
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.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
from django.contrib import admin
# Register your models here.
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
永远年轻,永远热泪盈眶
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册