Delete 0.3.1 version code
37
Dockerfile
|
@ -1,37 +0,0 @@
|
|||
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
|
339
LICENSE
|
@ -1,339 +0,0 @@
|
|||
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.
|
80
README.md
|
@ -1,80 +0,0 @@
|
|||
## 写在前面
|
||||
- 版本号变更 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)
|
||||
|
||||
|
||||
|
||||
|
813
connect.py
|
@ -1,813 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import sys
|
||||
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('utf8')
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import datetime
|
||||
import textwrap
|
||||
import getpass
|
||||
import readline
|
||||
import django
|
||||
import paramiko
|
||||
import errno
|
||||
import pyte
|
||||
import operator
|
||||
import struct, fcntl, signal, socket, select
|
||||
from io import open as copen
|
||||
import uuid
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
|
||||
if not django.get_version().startswith('1.6'):
|
||||
setup = django.setup()
|
||||
from django.contrib.sessions.models import Session
|
||||
from jumpserver.api import ServerError, User, Asset, PermRole, AssetGroup, get_object, mkdir, get_asset_info
|
||||
from jumpserver.api import logger, Log, TtyLog, get_role_key, CRYPTOR, bash, get_tmp_dir
|
||||
from jperm.perm_api import gen_resource, get_group_asset_perm, get_group_user_perm, user_have_perm, PermRole
|
||||
from jumpserver.settings import LOG_DIR, NAV_SORT_BY
|
||||
from jperm.ansible_api import MyRunner
|
||||
# from jlog.log_api import escapeString
|
||||
from jlog.models import ExecLog, FileLog
|
||||
from jlog.views import TermLogRecorder
|
||||
|
||||
login_user = get_object(User, username=getpass.getuser())
|
||||
try:
|
||||
remote_ip = os.environ.get('SSH_CLIENT').split()[0]
|
||||
except (IndexError, AttributeError):
|
||||
remote_ip = os.popen("who -m | awk '{ print $NF }'").read().strip('()\n')
|
||||
|
||||
try:
|
||||
import termios
|
||||
import tty
|
||||
except ImportError:
|
||||
print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m'
|
||||
time.sleep(3)
|
||||
sys.exit()
|
||||
|
||||
|
||||
def color_print(msg, color='red', exits=False):
|
||||
"""
|
||||
Print colorful string.
|
||||
颜色打印字符或者退出
|
||||
"""
|
||||
color_msg = {'blue': '\033[1;36m%s\033[0m',
|
||||
'green': '\033[1;32m%s\033[0m',
|
||||
'yellow': '\033[1;33m%s\033[0m',
|
||||
'red': '\033[1;31m%s\033[0m',
|
||||
'title': '\033[30;42m%s\033[0m',
|
||||
'info': '\033[32m%s\033[0m'}
|
||||
msg = color_msg.get(color, 'red') % msg
|
||||
print msg
|
||||
if exits:
|
||||
time.sleep(2)
|
||||
sys.exit()
|
||||
return msg
|
||||
|
||||
|
||||
def write_log(f, msg):
|
||||
msg = re.sub(r'[\r\n]', '\r\n', msg)
|
||||
f.write(msg)
|
||||
f.flush()
|
||||
|
||||
|
||||
class Tty(object):
|
||||
"""
|
||||
A virtual tty class
|
||||
一个虚拟终端类,实现连接ssh和记录日志,基类
|
||||
"""
|
||||
def __init__(self, user, asset, role, login_type='ssh'):
|
||||
self.username = user.username
|
||||
self.asset_name = asset.hostname
|
||||
self.ip = None
|
||||
self.port = 22
|
||||
self.ssh = None
|
||||
self.channel = None
|
||||
self.asset = asset
|
||||
self.user = user
|
||||
self.role = role
|
||||
self.remote_ip = ''
|
||||
self.login_type = login_type
|
||||
self.vim_flag = False
|
||||
self.vim_end_pattern = re.compile(r'\x1b\[\?1049', re.X)
|
||||
self.vim_data = ''
|
||||
self.stream = None
|
||||
self.screen = None
|
||||
self.__init_screen_stream()
|
||||
|
||||
def __init_screen_stream(self):
|
||||
"""
|
||||
初始化虚拟屏幕和字符流
|
||||
"""
|
||||
self.stream = pyte.ByteStream()
|
||||
self.screen = pyte.Screen(80, 24)
|
||||
self.stream.attach(self.screen)
|
||||
|
||||
@staticmethod
|
||||
def is_output(strings):
|
||||
newline_char = ['\n', '\r', '\r\n']
|
||||
for char in newline_char:
|
||||
if char in strings:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def command_parser(command):
|
||||
"""
|
||||
处理命令中如果有ps1或者mysql的特殊情况,极端情况下会有ps1和mysql
|
||||
:param command:要处理的字符传
|
||||
:return:返回去除PS1或者mysql字符串的结果
|
||||
"""
|
||||
result = None
|
||||
match = re.compile('\[?.*@.*\]?[\$#]\s').split(command)
|
||||
if match:
|
||||
# 只需要最后的一个PS1后面的字符串
|
||||
result = match[-1].strip()
|
||||
else:
|
||||
# PS1没找到,查找mysql
|
||||
match = re.split('mysql>\s', command)
|
||||
if match:
|
||||
# 只需要最后一个mysql后面的字符串
|
||||
result = match[-1].strip()
|
||||
return result
|
||||
|
||||
def deal_command(self, data):
|
||||
"""
|
||||
处理截获的命令
|
||||
:param data: 要处理的命令
|
||||
:return:返回最后的处理结果
|
||||
"""
|
||||
command = ''
|
||||
try:
|
||||
self.stream.feed(data)
|
||||
# 从虚拟屏幕中获取处理后的数据
|
||||
for line in reversed(self.screen.buffer):
|
||||
line_data = "".join(map(operator.attrgetter("data"), line)).strip()
|
||||
if len(line_data) > 0:
|
||||
parser_result = self.command_parser(line_data)
|
||||
if parser_result is not None:
|
||||
# 2个条件写一起会有错误的数据
|
||||
if len(parser_result) > 0:
|
||||
command = parser_result
|
||||
else:
|
||||
command = line_data
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
# 虚拟屏幕清空
|
||||
self.screen.reset()
|
||||
return command
|
||||
|
||||
def get_log(self):
|
||||
"""
|
||||
Logging user command and output.
|
||||
记录用户的日志
|
||||
"""
|
||||
tty_log_dir = os.path.join(LOG_DIR, 'tty')
|
||||
date_today = datetime.datetime.now()
|
||||
date_start = date_today.strftime('%Y%m%d')
|
||||
time_start = date_today.strftime('%H%M%S')
|
||||
today_connect_log_dir = os.path.join(tty_log_dir, date_start)
|
||||
log_file_path = os.path.join(today_connect_log_dir, '%s_%s_%s' % (self.username, self.asset_name, time_start))
|
||||
|
||||
try:
|
||||
mkdir(os.path.dirname(today_connect_log_dir), mode=777)
|
||||
mkdir(today_connect_log_dir, mode=777)
|
||||
except OSError:
|
||||
logger.debug('创建目录 %s 失败,请修改%s目录权限' % (today_connect_log_dir, tty_log_dir))
|
||||
raise ServerError('创建目录 %s 失败,请修改%s目录权限' % (today_connect_log_dir, tty_log_dir))
|
||||
|
||||
try:
|
||||
log_file_f = open(log_file_path + '.log', 'a')
|
||||
log_time_f = open(log_file_path + '.time', 'a')
|
||||
except IOError:
|
||||
logger.debug('创建tty日志文件失败, 请修改目录%s权限' % today_connect_log_dir)
|
||||
raise ServerError('创建tty日志文件失败, 请修改目录%s权限' % today_connect_log_dir)
|
||||
|
||||
if self.login_type == 'ssh': # 如果是ssh连接过来,记录connect.py的pid,web terminal记录为日志的id
|
||||
pid = os.getpid()
|
||||
self.remote_ip = remote_ip # 获取远端IP
|
||||
else:
|
||||
pid = 0
|
||||
|
||||
log = Log(user=self.username, host=self.asset_name, remote_ip=self.remote_ip, login_type=self.login_type,
|
||||
log_path=log_file_path, start_time=date_today, pid=pid)
|
||||
log.save()
|
||||
if self.login_type == 'web':
|
||||
log.pid = log.id # 设置log id为websocket的id, 然后kill时干掉websocket
|
||||
log.save()
|
||||
|
||||
log_file_f.write('Start at %s\r\n' % datetime.datetime.now())
|
||||
return log_file_f, log_time_f, log
|
||||
|
||||
def get_connect_info(self):
|
||||
"""
|
||||
获取需要登陆的主机的信息和映射用户的账号密码
|
||||
"""
|
||||
asset_info = get_asset_info(self.asset)
|
||||
role_key = get_role_key(self.user, self.role) # 获取角色的key,因为ansible需要权限是600,所以统一生成用户_角色key
|
||||
role_pass = CRYPTOR.decrypt(self.role.password)
|
||||
connect_info = {'user': self.user, 'asset': self.asset, 'ip': asset_info.get('ip'),
|
||||
'port': int(asset_info.get('port')), 'role_name': self.role.name,
|
||||
'role_pass': role_pass, 'role_key': role_key}
|
||||
logger.debug(connect_info)
|
||||
return connect_info
|
||||
|
||||
def get_connection(self):
|
||||
"""
|
||||
获取连接成功后的ssh
|
||||
"""
|
||||
connect_info = self.get_connect_info()
|
||||
|
||||
# 发起ssh连接请求 Make a ssh connection
|
||||
ssh = paramiko.SSHClient()
|
||||
# ssh.load_system_host_keys()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
role_key = connect_info.get('role_key')
|
||||
if role_key and os.path.isfile(role_key):
|
||||
try:
|
||||
ssh.connect(connect_info.get('ip'),
|
||||
port=connect_info.get('port'),
|
||||
username=connect_info.get('role_name'),
|
||||
password=connect_info.get('role_pass'),
|
||||
key_filename=role_key,
|
||||
look_for_keys=False)
|
||||
return ssh
|
||||
except (paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException):
|
||||
logger.warning(u'使用ssh key %s 失败, 尝试只使用密码' % role_key)
|
||||
pass
|
||||
|
||||
ssh.connect(connect_info.get('ip'),
|
||||
port=connect_info.get('port'),
|
||||
username=connect_info.get('role_name'),
|
||||
password=connect_info.get('role_pass'),
|
||||
allow_agent=False,
|
||||
look_for_keys=False)
|
||||
|
||||
except (paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException):
|
||||
raise ServerError('认证失败 Authentication Error.')
|
||||
except socket.error:
|
||||
raise ServerError('端口可能不对 Connect SSH Socket Port Error, Please Correct it.')
|
||||
else:
|
||||
self.ssh = ssh
|
||||
return ssh
|
||||
|
||||
|
||||
class SshTty(Tty):
|
||||
"""
|
||||
A virtual tty class
|
||||
一个虚拟终端类,实现连接ssh和记录日志
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_win_size():
|
||||
"""
|
||||
This function use to get the size of the windows!
|
||||
获得terminal窗口大小
|
||||
"""
|
||||
if 'TIOCGWINSZ' in dir(termios):
|
||||
TIOCGWINSZ = termios.TIOCGWINSZ
|
||||
else:
|
||||
TIOCGWINSZ = 1074295912L
|
||||
s = struct.pack('HHHH', 0, 0, 0, 0)
|
||||
x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s)
|
||||
return struct.unpack('HHHH', x)[0:2]
|
||||
|
||||
def set_win_size(self, sig, data):
|
||||
"""
|
||||
This function use to set the window size of the terminal!
|
||||
设置terminal窗口大小
|
||||
"""
|
||||
try:
|
||||
win_size = self.get_win_size()
|
||||
self.channel.resize_pty(height=win_size[0], width=win_size[1])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def posix_shell(self):
|
||||
"""
|
||||
Use paramiko channel connect server interactive.
|
||||
使用paramiko模块的channel,连接后端,进入交互式
|
||||
"""
|
||||
log_file_f, log_time_f, log = self.get_log()
|
||||
termlog = TermLogRecorder(User.objects.get(id=self.user.id))
|
||||
termlog.setid(log.id)
|
||||
old_tty = termios.tcgetattr(sys.stdin)
|
||||
pre_timestamp = time.time()
|
||||
data = ''
|
||||
input_mode = False
|
||||
try:
|
||||
tty.setraw(sys.stdin.fileno())
|
||||
tty.setcbreak(sys.stdin.fileno())
|
||||
self.channel.settimeout(0.0)
|
||||
|
||||
while True:
|
||||
try:
|
||||
r, w, e = select.select([self.channel, sys.stdin], [], [])
|
||||
flag = fcntl.fcntl(sys.stdin, fcntl.F_GETFL, 0)
|
||||
fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flag|os.O_NONBLOCK)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if self.channel in r:
|
||||
try:
|
||||
x = self.channel.recv(10240)
|
||||
if len(x) == 0:
|
||||
break
|
||||
|
||||
index = 0
|
||||
len_x = len(x)
|
||||
while index < len_x:
|
||||
try:
|
||||
n = os.write(sys.stdout.fileno(), x[index:])
|
||||
sys.stdout.flush()
|
||||
index += n
|
||||
except OSError as msg:
|
||||
if msg.errno == errno.EAGAIN:
|
||||
continue
|
||||
now_timestamp = time.time()
|
||||
termlog.write(x)
|
||||
termlog.recoder = False
|
||||
log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x)))
|
||||
log_time_f.flush()
|
||||
log_file_f.write(x)
|
||||
log_file_f.flush()
|
||||
pre_timestamp = now_timestamp
|
||||
log_file_f.flush()
|
||||
|
||||
self.vim_data += x
|
||||
if input_mode:
|
||||
data += x
|
||||
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
if sys.stdin in r:
|
||||
try:
|
||||
x = os.read(sys.stdin.fileno(), 4096)
|
||||
except OSError:
|
||||
pass
|
||||
termlog.recoder = True
|
||||
input_mode = True
|
||||
if self.is_output(str(x)):
|
||||
# 如果len(str(x)) > 1 说明是复制输入的
|
||||
if len(str(x)) > 1:
|
||||
data = x
|
||||
match = self.vim_end_pattern.findall(self.vim_data)
|
||||
if match:
|
||||
if self.vim_flag or len(match) == 2:
|
||||
self.vim_flag = False
|
||||
else:
|
||||
self.vim_flag = True
|
||||
elif not self.vim_flag:
|
||||
self.vim_flag = False
|
||||
data = self.deal_command(data)[0:200]
|
||||
if data is not None:
|
||||
TtyLog(log=log, datetime=datetime.datetime.now(), cmd=data).save()
|
||||
data = ''
|
||||
self.vim_data = ''
|
||||
input_mode = False
|
||||
|
||||
if len(x) == 0:
|
||||
break
|
||||
self.channel.send(x)
|
||||
|
||||
finally:
|
||||
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty)
|
||||
log_file_f.write('End time is %s' % datetime.datetime.now())
|
||||
log_file_f.close()
|
||||
log_time_f.close()
|
||||
termlog.save()
|
||||
log.filename = termlog.filename
|
||||
log.is_finished = True
|
||||
log.end_time = datetime.datetime.now()
|
||||
log.save()
|
||||
|
||||
def connect(self):
|
||||
"""
|
||||
Connect server.
|
||||
连接服务器
|
||||
"""
|
||||
# 发起ssh连接请求 Make a ssh connection
|
||||
ssh = self.get_connection()
|
||||
|
||||
transport = ssh.get_transport()
|
||||
transport.set_keepalive(30)
|
||||
transport.use_compression(True)
|
||||
|
||||
# 获取连接的隧道并设置窗口大小 Make a channel and set windows size
|
||||
global channel
|
||||
win_size = self.get_win_size()
|
||||
# self.channel = channel = ssh.invoke_shell(height=win_size[0], width=win_size[1], term='xterm')
|
||||
self.channel = channel = transport.open_session()
|
||||
channel.get_pty(term='xterm', height=win_size[0], width=win_size[1])
|
||||
channel.invoke_shell()
|
||||
try:
|
||||
signal.signal(signal.SIGWINCH, self.set_win_size)
|
||||
except:
|
||||
pass
|
||||
|
||||
self.posix_shell()
|
||||
|
||||
# Shutdown channel socket
|
||||
channel.close()
|
||||
ssh.close()
|
||||
|
||||
|
||||
class Nav(object):
|
||||
"""
|
||||
导航提示类
|
||||
"""
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
self.user_perm = get_group_user_perm(self.user)
|
||||
if NAV_SORT_BY == 'ip':
|
||||
self.perm_assets = sorted(self.user_perm.get('asset', []).keys(),
|
||||
key=lambda x: [int(num) for num in x.ip.split('.') if num.isdigit()])
|
||||
elif NAV_SORT_BY == 'hostname':
|
||||
self.perm_assets = self.natural_sort_hostname(self.user_perm.get('asset', []).keys())
|
||||
else:
|
||||
self.perm_assets = tuple(self.user_perm.get('asset', []))
|
||||
self.search_result = self.perm_assets
|
||||
self.perm_asset_groups = self.user_perm.get('asset_group', [])
|
||||
|
||||
def natural_sort_hostname(self, list):
|
||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
||||
alphanum_key = lambda x: [ convert(c) for c in re.split('([0-9]+)', x.hostname) ]
|
||||
return sorted(list, key = alphanum_key)
|
||||
|
||||
@staticmethod
|
||||
def print_nav():
|
||||
"""
|
||||
Print prompt
|
||||
打印提示导航
|
||||
"""
|
||||
msg = """\n\033[1;32m### 欢迎使用Jumpserver开源跳板机系统 ### \033[0m
|
||||
|
||||
1) 输入 \033[32mID\033[0m 直接登录 或 输入\033[32m部分 IP,主机名,备注\033[0m 进行搜索登录(如果唯一).
|
||||
2) 输入 \033[32m/\033[0m + \033[32mIP, 主机名 or 备注 \033[0m搜索. 如: /ip
|
||||
3) 输入 \033[32mP/p\033[0m 显示您有权限的主机.
|
||||
4) 输入 \033[32mG/g\033[0m 显示您有权限的主机组.
|
||||
5) 输入 \033[32mG/g\033[0m\033[0m + \033[32m组ID\033[0m 显示该组下主机. 如: g1
|
||||
6) 输入 \033[32mE/e\033[0m 批量执行命令.
|
||||
7) 输入 \033[32mU/u\033[0m 批量上传文件.
|
||||
8) 输入 \033[32mD/d\033[0m 批量下载文件.
|
||||
9) 输入 \033[32mH/h\033[0m 帮助.
|
||||
0) 输入 \033[32mQ/q\033[0m 退出.
|
||||
"""
|
||||
print textwrap.dedent(msg)
|
||||
|
||||
def get_asset_group_member(self, str_r):
|
||||
gid_pattern = re.compile(r'^g\d+$')
|
||||
|
||||
if gid_pattern.match(str_r):
|
||||
gid = int(str_r.lstrip('g'))
|
||||
# 获取资产组包含的资产
|
||||
asset_group = get_object(AssetGroup, id=gid)
|
||||
if asset_group and asset_group in self.perm_asset_groups:
|
||||
self.search_result = list(asset_group.asset_set.all())
|
||||
else:
|
||||
color_print('没有该资产组或没有权限')
|
||||
return
|
||||
|
||||
def search(self, str_r=''):
|
||||
# 搜索结果保存
|
||||
if str_r:
|
||||
try:
|
||||
id_ = int(str_r)
|
||||
if id_ < len(self.search_result):
|
||||
self.search_result = [self.search_result[id_]]
|
||||
return
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
except (ValueError, TypeError):
|
||||
# 匹配 ip, hostname, 备注
|
||||
str_r = str_r.lower()
|
||||
self.search_result = [asset for asset in self.perm_assets if str_r == str(asset.ip).lower()] or \
|
||||
[asset for asset in self.perm_assets if str_r in str(asset.ip).lower() \
|
||||
or str_r in str(asset.hostname).lower() \
|
||||
or str_r in str(asset.comment).lower()]
|
||||
else:
|
||||
# 如果没有输入就展现所有
|
||||
self.search_result = self.perm_assets
|
||||
|
||||
@staticmethod
|
||||
def truncate_str(str_, length=30):
|
||||
str_ = str_.decode('utf-8')
|
||||
if len(str_) > length:
|
||||
return str_[:14] + '..' + str_[-14:]
|
||||
else:
|
||||
return str_
|
||||
|
||||
@staticmethod
|
||||
def get_max_asset_property_length(assets, property_='hostname'):
|
||||
try:
|
||||
return max([len(getattr(asset, property_)) for asset in assets])
|
||||
except ValueError:
|
||||
return 30
|
||||
|
||||
def print_search_result(self):
|
||||
hostname_max_length = self.get_max_asset_property_length(self.search_result)
|
||||
line = '[%-3s] %-16s %-5s %-' + str(hostname_max_length) + 's %-10s %s'
|
||||
color_print(line % ('ID', 'IP', 'Port', 'Hostname', 'SysUser', 'Comment'), 'title')
|
||||
if hasattr(self.search_result, '__iter__'):
|
||||
for index, asset in enumerate(self.search_result):
|
||||
# 获取该资产信息
|
||||
asset_info = get_asset_info(asset)
|
||||
# 获取该资产包含的角色
|
||||
role = [str(role.name) for role in self.user_perm.get('asset').get(asset).get('role')]
|
||||
print line % (index, asset.ip, asset_info.get('port'),
|
||||
self.truncate_str(asset.hostname), str(role).replace("'", ''), asset.comment)
|
||||
print
|
||||
|
||||
def try_connect(self):
|
||||
try:
|
||||
asset = self.search_result[0]
|
||||
roles = list(self.user_perm.get('asset').get(asset).get('role'))
|
||||
if len(roles) == 1:
|
||||
role = roles[0]
|
||||
elif len(roles) > 1:
|
||||
print "\033[32m[ID] 系统用户\033[0m"
|
||||
for index, role in enumerate(roles):
|
||||
print "[%-2s] %s" % (index, role.name)
|
||||
print
|
||||
print "授权系统用户超过1个,请输入ID, q退出"
|
||||
try:
|
||||
role_index = raw_input("\033[1;32mID>:\033[0m ").strip()
|
||||
if role_index == 'q':
|
||||
return
|
||||
else:
|
||||
role = roles[int(role_index)]
|
||||
except IndexError:
|
||||
color_print('请输入正确ID', 'red')
|
||||
return
|
||||
else:
|
||||
color_print('没有映射用户', 'red')
|
||||
return
|
||||
|
||||
print('Connecting %s ...' % asset.hostname)
|
||||
ssh_tty = SshTty(login_user, asset, role)
|
||||
ssh_tty.connect()
|
||||
except (KeyError, ValueError):
|
||||
color_print('请输入正确ID', 'red')
|
||||
except ServerError, e:
|
||||
color_print(e, 'red')
|
||||
|
||||
def print_asset_group(self):
|
||||
"""
|
||||
打印用户授权的资产组
|
||||
"""
|
||||
user_asset_group_all = get_group_user_perm(self.user).get('asset_group', [])
|
||||
color_print('[%-3s] %-20s %s' % ('ID', '组名', '备注'), 'title')
|
||||
for asset_group in user_asset_group_all:
|
||||
print '[%-3s] %-15s %s' % (asset_group.id, asset_group.name, asset_group.comment)
|
||||
print
|
||||
|
||||
def exec_cmd(self):
|
||||
"""
|
||||
批量执行命令
|
||||
"""
|
||||
while True:
|
||||
roles = self.user_perm.get('role').keys()
|
||||
if len(roles) > 1: # 授权角色数大于1
|
||||
color_print('[%-2s] %-15s' % ('ID', '系统用户'), 'info')
|
||||
role_check = dict(zip(range(len(roles)), roles))
|
||||
|
||||
for i, r in role_check.items():
|
||||
print '[%-2s] %-15s' % (i, r.name)
|
||||
print
|
||||
print "请输入运行命令所关联系统用户的ID, q退出"
|
||||
|
||||
try:
|
||||
role_id = int(raw_input("\033[1;32mRole>:\033[0m ").strip())
|
||||
if role_id == 'q':
|
||||
break
|
||||
except (IndexError, ValueError):
|
||||
color_print('错误输入')
|
||||
else:
|
||||
role = role_check[int(role_id)]
|
||||
elif len(roles) == 1: # 授权角色数为1
|
||||
role = roles[0]
|
||||
else:
|
||||
color_print('当前用户未被授予角色,无法执行任何操作,如有疑问请联系管理员。')
|
||||
return
|
||||
assets = list(self.user_perm.get('role', {}).get(role).get('asset')) # 获取该用户,角色授权主机
|
||||
print "授权包含该系统用户的所有主机"
|
||||
for asset in assets:
|
||||
print ' %s' % asset.hostname
|
||||
print
|
||||
print "请输入主机名或ansible支持的pattern, 多个主机:分隔, q退出"
|
||||
pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip()
|
||||
if pattern == 'q':
|
||||
break
|
||||
else:
|
||||
res = gen_resource({'user': self.user, 'asset': assets, 'role': role}, perm=self.user_perm)
|
||||
runner = MyRunner(res)
|
||||
asset_name_str = ''
|
||||
print "匹配主机:"
|
||||
for inv in runner.inventory.get_hosts(pattern=pattern):
|
||||
print ' %s' % inv.name
|
||||
asset_name_str += '%s ' % inv.name
|
||||
print
|
||||
|
||||
while True:
|
||||
print "请输入执行的命令, 按q退出"
|
||||
command = raw_input("\033[1;32mCmds>:\033[0m ").strip()
|
||||
if command == 'q':
|
||||
break
|
||||
elif not command:
|
||||
color_print('命令不能为空...')
|
||||
continue
|
||||
runner.run('shell', command, pattern=pattern)
|
||||
ExecLog(host=asset_name_str, user=self.user.username, cmd=command, remote_ip=remote_ip,
|
||||
result=runner.results).save()
|
||||
for k, v in runner.results.items():
|
||||
if k == 'ok':
|
||||
for host, output in v.items():
|
||||
color_print("%s => %s" % (host, 'Ok'), 'green')
|
||||
print output
|
||||
print
|
||||
else:
|
||||
for host, output in v.items():
|
||||
color_print("%s => %s" % (host, k), 'red')
|
||||
color_print(output, 'red')
|
||||
print
|
||||
print "~o~ Task finished ~o~"
|
||||
print
|
||||
|
||||
def upload(self):
|
||||
while True:
|
||||
try:
|
||||
print "进入批量上传模式"
|
||||
print "请输入主机名或ansible支持的pattern, 多个主机:分隔 q退出"
|
||||
pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip()
|
||||
if pattern == 'q':
|
||||
break
|
||||
else:
|
||||
assets = self.user_perm.get('asset').keys()
|
||||
res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm)
|
||||
runner = MyRunner(res)
|
||||
asset_name_str = ''
|
||||
print "匹配主机:"
|
||||
for inv in runner.inventory.get_hosts(pattern=pattern):
|
||||
print inv.name
|
||||
asset_name_str += '%s ' % inv.name
|
||||
|
||||
if not asset_name_str:
|
||||
color_print('没有匹配主机')
|
||||
continue
|
||||
tmp_dir = get_tmp_dir()
|
||||
logger.debug('Upload tmp dir: %s' % tmp_dir)
|
||||
os.chdir(tmp_dir)
|
||||
bash('rz')
|
||||
filename_str = ' '.join(os.listdir(tmp_dir))
|
||||
if not filename_str:
|
||||
color_print("上传文件为空")
|
||||
continue
|
||||
logger.debug('上传文件: %s' % filename_str)
|
||||
|
||||
runner = MyRunner(res)
|
||||
runner.run('copy', module_args='src=%s dest=%s directory_mode'
|
||||
% (tmp_dir, '/tmp'), pattern=pattern)
|
||||
ret = runner.results
|
||||
FileLog(user=self.user.name, host=asset_name_str, filename=filename_str,
|
||||
remote_ip=remote_ip, type='upload', result=ret).save()
|
||||
logger.debug('Upload file: %s' % ret)
|
||||
if ret.get('failed'):
|
||||
error = '上传目录: %s \n上传失败: [ %s ] \n上传成功 [ %s ]' % (tmp_dir,
|
||||
', '.join(ret.get('failed').keys()),
|
||||
', '.join(ret.get('ok').keys()))
|
||||
color_print(error)
|
||||
else:
|
||||
msg = '上传目录: %s \n传送成功 [ %s ]' % (tmp_dir, ', '.join(ret.get('ok').keys()))
|
||||
color_print(msg, 'green')
|
||||
print
|
||||
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def download(self):
|
||||
while True:
|
||||
try:
|
||||
print "进入批量下载模式"
|
||||
print "请输入主机名或ansible支持的pattern, 多个主机:分隔,q退出"
|
||||
pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip()
|
||||
if pattern == 'q':
|
||||
break
|
||||
else:
|
||||
assets = self.user_perm.get('asset').keys()
|
||||
res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm)
|
||||
runner = MyRunner(res)
|
||||
asset_name_str = ''
|
||||
print "匹配主机:\n"
|
||||
for inv in runner.inventory.get_hosts(pattern=pattern):
|
||||
asset_name_str += '%s ' % inv.name
|
||||
print ' %s' % inv.name
|
||||
if not asset_name_str:
|
||||
color_print('没有匹配主机')
|
||||
continue
|
||||
print
|
||||
while True:
|
||||
tmp_dir = get_tmp_dir()
|
||||
logger.debug('Download tmp dir: %s' % tmp_dir)
|
||||
print "请输入文件路径(不支持目录)"
|
||||
file_path = raw_input("\033[1;32mPath>:\033[0m ").strip()
|
||||
if file_path == 'q':
|
||||
break
|
||||
|
||||
if not file_path:
|
||||
color_print("文件路径为空")
|
||||
continue
|
||||
|
||||
runner.run('fetch', module_args='src=%s dest=%s' % (file_path, tmp_dir), pattern=pattern)
|
||||
ret = runner.results
|
||||
FileLog(user=self.user.name, host=asset_name_str, filename=file_path, type='download',
|
||||
remote_ip=remote_ip, result=ret).save()
|
||||
logger.debug('Download file result: %s' % ret)
|
||||
os.chdir('/tmp')
|
||||
tmp_dir_name = os.path.basename(tmp_dir)
|
||||
if not os.listdir(tmp_dir):
|
||||
color_print('下载全部失败')
|
||||
continue
|
||||
bash('tar czf %s.tar.gz %s && sz %s.tar.gz' % (tmp_dir, tmp_dir_name, tmp_dir))
|
||||
|
||||
if ret.get('failed'):
|
||||
error = '文件名称: %s \n下载失败: [ %s ] \n下载成功 [ %s ]' % \
|
||||
('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('failed').keys()), ', '.join(ret.get('ok').keys()))
|
||||
color_print(error)
|
||||
else:
|
||||
msg = '文件名称: %s \n下载成功 [ %s ]' % ('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('ok').keys()))
|
||||
color_print(msg, 'green')
|
||||
print
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
he he
|
||||
主程序
|
||||
"""
|
||||
if not login_user: # 判断用户是否存在
|
||||
color_print('没有该用户,或许你是以root运行的 No that user.', exits=True)
|
||||
|
||||
if not login_user.is_active:
|
||||
color_print('您的用户已禁用,请联系管理员.', exits=True)
|
||||
|
||||
gid_pattern = re.compile(r'^g\d+$')
|
||||
nav = Nav(login_user)
|
||||
nav.print_nav()
|
||||
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
option = raw_input("\033[1;32mOpt or ID>:\033[0m ").strip()
|
||||
except EOFError:
|
||||
nav.print_nav()
|
||||
continue
|
||||
except KeyboardInterrupt:
|
||||
sys.exit(0)
|
||||
if option in ['P', 'p', '\n', '']:
|
||||
nav.search()
|
||||
nav.print_search_result()
|
||||
continue
|
||||
if option.startswith('/'):
|
||||
nav.search(option.lstrip('/'))
|
||||
nav.print_search_result()
|
||||
elif gid_pattern.match(option):
|
||||
nav.get_asset_group_member(str_r=option)
|
||||
nav.print_search_result()
|
||||
elif option in ['G', 'g']:
|
||||
nav.print_asset_group()
|
||||
continue
|
||||
elif option in ['E', 'e']:
|
||||
nav.exec_cmd()
|
||||
continue
|
||||
elif option in ['U', 'u']:
|
||||
nav.upload()
|
||||
elif option in ['D', 'd']:
|
||||
nav.download()
|
||||
elif option in ['H', 'h']:
|
||||
nav.print_nav()
|
||||
elif option in ['Q', 'q', 'exit']:
|
||||
sys.exit()
|
||||
else:
|
||||
nav.search(option)
|
||||
if len(nav.search_result) == 1:
|
||||
print('Only match Host: %s ' % nav.search_result[0].hostname)
|
||||
nav.try_connect()
|
||||
else:
|
||||
nav.print_search_result()
|
||||
|
||||
except IndexError, e:
|
||||
color_print(e)
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,17 +0,0 @@
|
|||
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
|
127
docs/README.md
|
@ -1,127 +0,0 @@
|
|||
快速安装
|
||||
------
|
||||
####环境
|
||||
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 测试文件的上传下载,日志审计 - 上传下载 查看上传下载记录
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# 使用Nginx搭建SSL配置
|
||||
|
||||
跳板机是所有服务器的入口,所以,它的安全至关重要。因此,建议把`Jumpserver`搭建在内网环境中,并且加上SSL证书,保证数据传输的安全。
|
||||
|
||||
## nginx的安装
|
||||
|
||||
不同的操作系统及版本,安装方法都不太一样。我们以`Debian`为例。
|
||||
|
||||
```
|
||||
apt-get update
|
||||
apt-get install -y nginx
|
||||
```
|
||||
|
||||
更多安装示例请参考 [Nginx官方安装指南](https://www.nginx.com/resources/wiki/start/topics/tutorials/install/)
|
||||
|
||||
## Nginx中的SSL的配置
|
||||
|
||||
* 编辑 `/etc/nginx/sites-enabled/default` 或者指定的`Jumpserver`的配置文件
|
||||
|
||||
* 示例如下
|
||||
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
listen 80;
|
||||
server_name YOUR_DOMAIN;
|
||||
ssl_certificate YOUR_DOMAIN_CRT;
|
||||
ssl_certificate_key YOUR_DOMAIN_KEY;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl on ;
|
||||
|
||||
if ($ssl_protocol = "") {
|
||||
rewrite ^ https://$host$request_uri? permanent;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header Connection "";
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://JUMPSERVER_HOST:WEB_PORT;
|
||||
}
|
||||
|
||||
location /_ws/ {
|
||||
keepalive_timeout 600s;
|
||||
send_timeout 600s;
|
||||
proxy_connect_timeout 7d;
|
||||
proxy_send_timeout 7d;
|
||||
proxy_read_timeout 7d;
|
||||
rewrite ^/_ws(/.*)$ $1 break;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass http://JUMPSERVER_HOST:WS_PORT;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
* 请替换如下表格的关键字
|
||||
|
||||
|
||||
关键字 | 示例 | 说明
|
||||
------------- | ------------- |-------
|
||||
`YOUR_DOMAIN` | example.com | `Jumpserver`的域名
|
||||
`YOUR_DOMAIN_CRT` | /etc/nginx/certs/example.crt | SSL证书的CRT文件
|
||||
`YOUR_DOMAIN_KEY` | /etc/nginx/certs/example.key | SSL证书的KEY文件
|
||||
`JUMPSERVER_HOST` | 127.0.0.1 | `Jumpserver`服务器IP
|
||||
`WEB_PORT ` | 80 | `Jumpserver`网页监听端口
|
||||
`WS_PORT ` | 3000 | websocket端口,`Jumpserver` 默认为3000
|
||||
|
||||
* 此配置会强制使用`https`, 建议加上(即if判断的那三行)。
|
10
init.sh
|
@ -1,10 +0,0 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
|
||||
trap '' SIGINT
|
||||
base_dir=$(dirname $0)
|
||||
|
||||
export LANG='zh_CN.UTF-8'
|
||||
python $base_dir/connect.py
|
||||
|
||||
exit
|
|
@ -1,36 +0,0 @@
|
|||
# coding: utf8
|
||||
|
||||
Jumpserver开发者文档
|
||||
|
||||
开发规范:
|
||||
1. 遵守PE8规范 1) 命名规范 2) 导入模块规范 3) 空行规范 4) 长度规范
|
||||
2. 缩进统一4个空格
|
||||
3. 变量命名明了易懂多个单词下划线隔开
|
||||
4. 注释到位
|
||||
|
||||
|
||||
框架说明:
|
||||
1. 项目名称 Jumpserver
|
||||
2. APP:
|
||||
juser 用户管理
|
||||
jasset 资产管理(设备管理)
|
||||
jpermission 授权管理
|
||||
jlog 日志管理
|
||||
3. connect.py 用户登录入口程序
|
||||
4. logs 日志保存目录
|
||||
5. jumpserver.conf 配置文件
|
||||
6. docs 文档目录
|
||||
7. static 静态文件目录
|
||||
8. templates 模板目录
|
||||
|
||||
|
||||
connect.py逻辑说明:
|
||||
用户登录系统,运行该脚本,p调用get_user_host函数查看有权限的服务器ip
|
||||
输入部分IP,verify_connect匹配该部分ip,如果是匹配到多个,就显示ip
|
||||
匹配到0了就显示没有权限或者主机,
|
||||
匹配到1个则继续
|
||||
查询该服务器是否支持ldap 如果是,获得ldap用户密码登陆
|
||||
如果否,查询授权表,查看该服务器授权的系统用户,并返回对应账号密码,登陆
|
||||
connect函数是登陆函数,采用paramiko 使用channel登陆,posix_shell 来完成交互,并记录日志
|
||||
signal模块来完成窗口改变导致的tty大小随之改变
|
||||
PyCrypt是对称加密类
|
|
@ -1,25 +0,0 @@
|
|||
[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
|
|
@ -1,29 +0,0 @@
|
|||
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
|
|
@ -1,19 +0,0 @@
|
|||
#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
|
|
@ -1,53 +0,0 @@
|
|||
#!/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
|
|
@ -1,29 +0,0 @@
|
|||
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:::::
|
|
@ -1,146 +0,0 @@
|
|||
# $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
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
adduser $@
|
|
@ -1,2 +0,0 @@
|
|||
#!/bin/sh
|
||||
deluser --remove-home $3
|
|
@ -1,594 +0,0 @@
|
|||
# -*-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
|
|
@ -1,9 +0,0 @@
|
|||
- 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
|
|
@ -1,307 +0,0 @@
|
|||
#!/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()
|
113
install/next.py
|
@ -1,113 +0,0 @@
|
|||
#!/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()
|
|
@ -1,19 +0,0 @@
|
|||
#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
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,403 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# coding:utf-8
|
||||
from django import forms
|
||||
|
||||
from jasset.models import IDC, Asset, AssetGroup
|
||||
|
||||
|
||||
class AssetForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Asset
|
||||
|
||||
fields = [
|
||||
"ip", "other_ip", "hostname", "port", "group", "username", "password", "use_default_auth",
|
||||
"idc", "mac", "remote_ip", "brand", "cpu", "memory", "disk", "system_type", "system_version",
|
||||
"cabinet", "position", "number", "status", "asset_type", "env", "sn", "is_active", "comment",
|
||||
"system_arch"
|
||||
]
|
||||
|
||||
|
||||
class AssetGroupForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = AssetGroup
|
||||
fields = [
|
||||
"name", "comment"
|
||||
]
|
||||
|
||||
|
||||
class IdcForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = IDC
|
||||
fields = ['name', "bandwidth", "operator", 'linkman', 'phone', 'address', 'network', 'comment']
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'placeholder': 'Name'}),
|
||||
'network': forms.Textarea(
|
||||
attrs={'placeholder': '192.168.1.0/24\n192.168.2.0/24'})
|
||||
}
|
||||
|
||||
|
111
jasset/models.py
|
@ -1,111 +0,0 @@
|
|||
# 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
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,24 +0,0 @@
|
|||
# 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'),
|
||||
)
|
577
jasset/views.py
|
@ -1,577 +0,0 @@
|
|||
# coding:utf-8
|
||||
|
||||
from django.db.models import Q
|
||||
from jasset.asset_api import *
|
||||
from jumpserver.api import *
|
||||
from jumpserver.models import Setting
|
||||
from jasset.forms import AssetForm, IdcForm
|
||||
from jasset.models import Asset, IDC, AssetGroup, ASSET_TYPE, ASSET_STATUS
|
||||
from jperm.perm_api import get_group_asset_perm, get_group_user_perm
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def group_add(request):
|
||||
"""
|
||||
Group add view
|
||||
添加资产组
|
||||
"""
|
||||
header_title, path1, path2 = u'添加资产组', u'资产管理', u'添加资产组'
|
||||
asset_all = Asset.objects.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
name = request.POST.get('name', '')
|
||||
asset_select = request.POST.getlist('asset_select', [])
|
||||
comment = request.POST.get('comment', '')
|
||||
|
||||
try:
|
||||
if not name:
|
||||
emg = u'组名不能为空'
|
||||
raise ServerError(emg)
|
||||
|
||||
asset_group_test = get_object(AssetGroup, name=name)
|
||||
if asset_group_test:
|
||||
emg = u"该组名 %s 已存在" % name
|
||||
raise ServerError(emg)
|
||||
|
||||
except ServerError:
|
||||
pass
|
||||
|
||||
else:
|
||||
db_add_group(name=name, comment=comment, asset_select=asset_select)
|
||||
smg = u"主机组 %s 添加成功" % name
|
||||
|
||||
return my_render('jasset/group_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def group_edit(request):
|
||||
"""
|
||||
Group edit view
|
||||
编辑资产组
|
||||
"""
|
||||
header_title, path1, path2 = u'编辑主机组', u'资产管理', u'编辑主机组'
|
||||
group_id = request.GET.get('id', '')
|
||||
group = get_object(AssetGroup, id=group_id)
|
||||
|
||||
asset_all = Asset.objects.all()
|
||||
asset_select = Asset.objects.filter(group=group)
|
||||
asset_no_select = [a for a in asset_all if a not in asset_select]
|
||||
|
||||
if request.method == 'POST':
|
||||
name = request.POST.get('name', '')
|
||||
asset_select = request.POST.getlist('asset_select', [])
|
||||
comment = request.POST.get('comment', '')
|
||||
|
||||
try:
|
||||
if not name:
|
||||
emg = u'组名不能为空'
|
||||
raise ServerError(emg)
|
||||
|
||||
if group.name != name:
|
||||
asset_group_test = get_object(AssetGroup, name=name)
|
||||
if asset_group_test:
|
||||
emg = u"该组名 %s 已存在" % name
|
||||
raise ServerError(emg)
|
||||
|
||||
except ServerError:
|
||||
pass
|
||||
|
||||
else:
|
||||
group.asset_set.clear()
|
||||
db_update_group(id=group_id, name=name, comment=comment, asset_select=asset_select)
|
||||
smg = u"主机组 %s 添加成功" % name
|
||||
|
||||
return HttpResponseRedirect(reverse('asset_group_list'))
|
||||
|
||||
return my_render('jasset/group_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def group_list(request):
|
||||
"""
|
||||
list asset group
|
||||
列出资产组
|
||||
"""
|
||||
header_title, path1, path2 = u'查看资产组', u'资产管理', u'查看资产组'
|
||||
keyword = request.GET.get('keyword', '')
|
||||
asset_group_list = AssetGroup.objects.all()
|
||||
group_id = request.GET.get('id')
|
||||
if group_id:
|
||||
asset_group_list = asset_group_list.filter(id=group_id)
|
||||
if keyword:
|
||||
asset_group_list = asset_group_list.filter(Q(name__contains=keyword) | Q(comment__contains=keyword))
|
||||
|
||||
asset_group_list, p, asset_groups, page_range, current_page, show_first, show_end = pages(asset_group_list, request)
|
||||
return my_render('jasset/group_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def group_del(request):
|
||||
"""
|
||||
Group delete view
|
||||
删除主机组
|
||||
"""
|
||||
group_ids = request.GET.get('id', '')
|
||||
group_id_list = group_ids.split(',')
|
||||
|
||||
for group_id in group_id_list:
|
||||
AssetGroup.objects.filter(id=group_id).delete()
|
||||
|
||||
return HttpResponse(u'删除成功')
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_add(request):
|
||||
"""
|
||||
Asset add view
|
||||
添加资产
|
||||
"""
|
||||
header_title, path1, path2 = u'添加资产', u'资产管理', u'添加资产'
|
||||
asset_group_all = AssetGroup.objects.all()
|
||||
af = AssetForm()
|
||||
default_setting = get_object(Setting, name='default')
|
||||
default_port = default_setting.field2 if default_setting else ''
|
||||
if request.method == 'POST':
|
||||
af_post = AssetForm(request.POST)
|
||||
ip = request.POST.get('ip', '')
|
||||
hostname = request.POST.get('hostname', '')
|
||||
|
||||
is_active = True if request.POST.get('is_active') == '1' else False
|
||||
use_default_auth = request.POST.get('use_default_auth', '')
|
||||
try:
|
||||
if Asset.objects.filter(hostname=unicode(hostname)):
|
||||
error = u'该主机名 %s 已存在!' % hostname
|
||||
raise ServerError(error)
|
||||
if len(hostname) > 54:
|
||||
error = u"主机名长度不能超过53位!"
|
||||
raise ServerError(error)
|
||||
except ServerError:
|
||||
pass
|
||||
else:
|
||||
if af_post.is_valid():
|
||||
asset_save = af_post.save(commit=False)
|
||||
if not use_default_auth:
|
||||
password = request.POST.get('password', '')
|
||||
password_encode = CRYPTOR.encrypt(password)
|
||||
asset_save.password = password_encode
|
||||
if not ip:
|
||||
asset_save.ip = hostname
|
||||
asset_save.is_active = True if is_active else False
|
||||
asset_save.save()
|
||||
af_post.save_m2m()
|
||||
|
||||
msg = u'主机 %s 添加成功' % hostname
|
||||
else:
|
||||
esg = u'主机 %s 添加失败' % hostname
|
||||
|
||||
return my_render('jasset/asset_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_add_batch(request):
|
||||
header_title, path1, path2 = u'添加资产', u'资产管理', u'批量添加'
|
||||
return my_render('jasset/asset_add_batch.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_del(request):
|
||||
"""
|
||||
del a asset
|
||||
删除主机
|
||||
"""
|
||||
asset_id = request.GET.get('id', '')
|
||||
if asset_id:
|
||||
Asset.objects.filter(id=asset_id).delete()
|
||||
|
||||
if request.method == 'POST':
|
||||
asset_batch = request.GET.get('arg', '')
|
||||
asset_id_all = str(request.POST.get('asset_id_all', ''))
|
||||
|
||||
if asset_batch:
|
||||
for asset_id in asset_id_all.split(','):
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
asset.delete()
|
||||
|
||||
return HttpResponse(u'删除成功')
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def asset_edit(request):
|
||||
"""
|
||||
edit a asset
|
||||
修改主机
|
||||
"""
|
||||
header_title, path1, path2 = u'修改资产', u'资产管理', u'修改资产'
|
||||
|
||||
asset_id = request.GET.get('id', '')
|
||||
username = request.user.username
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
password_old = asset.password
|
||||
# asset_old = copy_model_instance(asset)
|
||||
af = AssetForm(instance=asset)
|
||||
if request.method == 'POST':
|
||||
af_post = AssetForm(request.POST, instance=asset)
|
||||
ip = request.POST.get('ip', '')
|
||||
hostname = request.POST.get('hostname', '')
|
||||
password = request.POST.get('password', '')
|
||||
is_active = True if request.POST.get('is_active') == '1' else False
|
||||
use_default_auth = request.POST.get('use_default_auth', '')
|
||||
try:
|
||||
asset_test = get_object(Asset, hostname=hostname)
|
||||
if asset_test and asset_id != unicode(asset_test.id):
|
||||
emg = u'该主机名 %s 已存在!' % hostname
|
||||
raise ServerError(emg)
|
||||
if len(hostname) > 54:
|
||||
emg = u'主机名长度不能超过54位!'
|
||||
raise ServerError(emg)
|
||||
else:
|
||||
if af_post.is_valid():
|
||||
af_save = af_post.save(commit=False)
|
||||
if use_default_auth:
|
||||
af_save.username = ''
|
||||
af_save.password = ''
|
||||
# af_save.port = None
|
||||
else:
|
||||
if password:
|
||||
password_encode = CRYPTOR.encrypt(password)
|
||||
af_save.password = password_encode
|
||||
else:
|
||||
af_save.password = password_old
|
||||
af_save.is_active = True if is_active else False
|
||||
af_save.save()
|
||||
af_post.save_m2m()
|
||||
# asset_new = get_object(Asset, id=asset_id)
|
||||
# asset_diff_one(asset_old, asset_new)
|
||||
info = asset_diff(af_post.__dict__.get('initial'), request.POST)
|
||||
db_asset_alert(asset, username, info)
|
||||
|
||||
smg = u'主机 %s 修改成功' % ip
|
||||
else:
|
||||
emg = u'主机 %s 修改失败' % ip
|
||||
raise ServerError(emg)
|
||||
except ServerError as e:
|
||||
error = e.message
|
||||
return my_render('jasset/asset_edit.html', locals(), request)
|
||||
return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id)
|
||||
|
||||
return my_render('jasset/asset_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('user')
|
||||
def asset_list(request):
|
||||
"""
|
||||
asset list view
|
||||
"""
|
||||
header_title, path1, path2 = u'查看资产', u'资产管理', u'查看资产'
|
||||
username = request.user.username
|
||||
user_perm = request.session['role_id']
|
||||
idc_all = IDC.objects.filter()
|
||||
asset_group_all = AssetGroup.objects.all()
|
||||
asset_types = ASSET_TYPE
|
||||
asset_status = ASSET_STATUS
|
||||
idc_name = request.GET.get('idc', '')
|
||||
group_name = request.GET.get('group', '')
|
||||
asset_type = request.GET.get('asset_type', '')
|
||||
status = request.GET.get('status', '')
|
||||
keyword = request.GET.get('keyword', '')
|
||||
export = request.GET.get("export", False)
|
||||
group_id = request.GET.get("group_id", '')
|
||||
idc_id = request.GET.get("idc_id", '')
|
||||
asset_id_all = request.GET.getlist("id", '')
|
||||
|
||||
if group_id:
|
||||
group = get_object(AssetGroup, id=group_id)
|
||||
if group:
|
||||
asset_find = Asset.objects.filter(group=group)
|
||||
elif idc_id:
|
||||
idc = get_object(IDC, id=idc_id)
|
||||
if idc:
|
||||
asset_find = Asset.objects.filter(idc=idc)
|
||||
else:
|
||||
if user_perm != 0:
|
||||
asset_find = Asset.objects.all()
|
||||
else:
|
||||
asset_id_all = []
|
||||
user = get_object(User, username=username)
|
||||
asset_perm = get_group_user_perm(user) if user else {'asset': ''}
|
||||
user_asset_perm = asset_perm['asset'].keys()
|
||||
for asset in user_asset_perm:
|
||||
asset_id_all.append(asset.id)
|
||||
asset_find = Asset.objects.filter(pk__in=asset_id_all)
|
||||
asset_group_all = list(asset_perm['asset_group'])
|
||||
|
||||
if idc_name:
|
||||
asset_find = asset_find.filter(idc__name__contains=idc_name)
|
||||
|
||||
if group_name:
|
||||
asset_find = asset_find.filter(group__name__contains=group_name)
|
||||
|
||||
if asset_type:
|
||||
asset_find = asset_find.filter(asset_type__contains=asset_type)
|
||||
|
||||
if status:
|
||||
asset_find = asset_find.filter(status__contains=status)
|
||||
|
||||
if keyword:
|
||||
asset_find = asset_find.filter(
|
||||
Q(hostname__contains=keyword) |
|
||||
Q(other_ip__contains=keyword) |
|
||||
Q(ip__contains=keyword) |
|
||||
Q(remote_ip__contains=keyword) |
|
||||
Q(comment__contains=keyword) |
|
||||
Q(username__contains=keyword) |
|
||||
Q(group__name__contains=keyword) |
|
||||
Q(cpu__contains=keyword) |
|
||||
Q(memory__contains=keyword) |
|
||||
Q(disk__contains=keyword) |
|
||||
Q(brand__contains=keyword) |
|
||||
Q(cabinet__contains=keyword) |
|
||||
Q(sn__contains=keyword) |
|
||||
Q(system_type__contains=keyword) |
|
||||
Q(system_version__contains=keyword))
|
||||
|
||||
if export:
|
||||
if asset_id_all:
|
||||
asset_find = []
|
||||
for asset_id in asset_id_all:
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
asset_find.append(asset)
|
||||
s = write_excel(asset_find)
|
||||
if s[0]:
|
||||
file_name = s[1]
|
||||
smg = u'excel文件已生成,请点击下载!'
|
||||
return my_render('jasset/asset_excel_download.html', locals(), request)
|
||||
assets_list, p, assets, page_range, current_page, show_first, show_end = pages(asset_find, request)
|
||||
if user_perm != 0:
|
||||
return my_render('jasset/asset_list.html', locals(), request)
|
||||
else:
|
||||
return my_render('jasset/asset_cu_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_edit_batch(request):
|
||||
af = AssetForm()
|
||||
name = request.user.username
|
||||
asset_group_all = AssetGroup.objects.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
env = request.POST.get('env', '')
|
||||
idc_id = request.POST.get('idc', '')
|
||||
port = request.POST.get('port', '')
|
||||
use_default_auth = request.POST.get('use_default_auth', '')
|
||||
username = request.POST.get('username', '')
|
||||
password = request.POST.get('password', '')
|
||||
group = request.POST.getlist('group', [])
|
||||
cabinet = request.POST.get('cabinet', '')
|
||||
comment = request.POST.get('comment', '')
|
||||
asset_id_all = unicode(request.GET.get('asset_id_all', ''))
|
||||
asset_id_all = asset_id_all.split(',')
|
||||
for asset_id in asset_id_all:
|
||||
alert_list = []
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
if env:
|
||||
if asset.env != env:
|
||||
asset.env = env
|
||||
alert_list.append([u'运行环境', asset.env, env])
|
||||
if idc_id:
|
||||
idc = get_object(IDC, id=idc_id)
|
||||
name_old = asset.idc.name if asset.idc else u''
|
||||
if idc and idc.name != name_old:
|
||||
asset.idc = idc
|
||||
alert_list.append([u'机房', name_old, idc.name])
|
||||
if port:
|
||||
if unicode(asset.port) != port:
|
||||
asset.port = port
|
||||
alert_list.append([u'端口号', asset.port, port])
|
||||
|
||||
if use_default_auth:
|
||||
if use_default_auth == 'default':
|
||||
asset.use_default_auth = 1
|
||||
asset.username = ''
|
||||
asset.password = ''
|
||||
alert_list.append([u'使用默认管理账号', asset.use_default_auth, u'默认'])
|
||||
elif use_default_auth == 'user_passwd':
|
||||
asset.use_default_auth = 0
|
||||
asset.username = username
|
||||
password_encode = CRYPTOR.encrypt(password)
|
||||
asset.password = password_encode
|
||||
alert_list.append([u'使用默认管理账号', asset.use_default_auth, username])
|
||||
if group:
|
||||
group_new, group_old, group_new_name, group_old_name = [], asset.group.all(), [], []
|
||||
for group_id in group:
|
||||
g = get_object(AssetGroup, id=group_id)
|
||||
if g:
|
||||
group_new.append(g)
|
||||
if not set(group_new) < set(group_old):
|
||||
group_instance = list(set(group_new) | set(group_old))
|
||||
for g in group_instance:
|
||||
group_new_name.append(g.name)
|
||||
for g in group_old:
|
||||
group_old_name.append(g.name)
|
||||
asset.group = group_instance
|
||||
alert_list.append([u'主机组', ','.join(group_old_name), ','.join(group_new_name)])
|
||||
if cabinet:
|
||||
if asset.cabinet != cabinet:
|
||||
asset.cabinet = cabinet
|
||||
alert_list.append([u'机柜号', asset.cabinet, cabinet])
|
||||
if comment:
|
||||
if asset.comment != comment:
|
||||
asset.comment = comment
|
||||
alert_list.append([u'备注', asset.comment, comment])
|
||||
asset.save()
|
||||
|
||||
if alert_list:
|
||||
recode_name = unicode(name) + ' - ' + u'批量'
|
||||
AssetRecord.objects.create(asset=asset, username=recode_name, content=alert_list)
|
||||
return my_render('jasset/asset_update_status.html', locals(), request)
|
||||
|
||||
return my_render('jasset/asset_edit_batch.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_detail(request):
|
||||
"""
|
||||
Asset detail view
|
||||
"""
|
||||
header_title, path1, path2 = u'主机详细信息', u'资产管理', u'主机详情'
|
||||
asset_id = request.GET.get('id', '')
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
perm_info = get_group_asset_perm(asset)
|
||||
log = Log.objects.filter(host=asset.hostname)
|
||||
if perm_info:
|
||||
user_perm = []
|
||||
for perm, value in perm_info.items():
|
||||
if perm == 'user':
|
||||
for user, role_dic in value.items():
|
||||
user_perm.append([user, role_dic.get('role', '')])
|
||||
elif perm == 'user_group' or perm == 'rule':
|
||||
user_group_perm = value
|
||||
print perm_info
|
||||
|
||||
asset_record = AssetRecord.objects.filter(asset=asset).order_by('-alert_time')
|
||||
|
||||
return my_render('jasset/asset_detail.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_update(request):
|
||||
"""
|
||||
Asset update host info via ansible view
|
||||
"""
|
||||
asset_id = request.GET.get('id', '')
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
name = request.user.username
|
||||
if not asset:
|
||||
return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id)
|
||||
else:
|
||||
asset_ansible_update([asset], name)
|
||||
return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_update_batch(request):
|
||||
if request.method == 'POST':
|
||||
arg = request.GET.get('arg', '')
|
||||
name = unicode(request.user.username) + ' - ' + u'自动更新'
|
||||
if arg == 'all':
|
||||
asset_list = Asset.objects.all()
|
||||
else:
|
||||
asset_list = []
|
||||
asset_id_all = unicode(request.POST.get('asset_id_all', ''))
|
||||
asset_id_all = asset_id_all.split(',')
|
||||
for asset_id in asset_id_all:
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
asset_list.append(asset)
|
||||
asset_ansible_update(asset_list, name)
|
||||
return HttpResponse(u'批量更新成功!')
|
||||
return HttpResponse(u'批量更新成功!')
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def idc_add(request):
|
||||
"""
|
||||
IDC add view
|
||||
"""
|
||||
header_title, path1, path2 = u'添加IDC', u'资产管理', u'添加IDC'
|
||||
if request.method == 'POST':
|
||||
idc_form = IdcForm(request.POST)
|
||||
if idc_form.is_valid():
|
||||
idc_name = idc_form.cleaned_data['name']
|
||||
|
||||
if IDC.objects.filter(name=idc_name):
|
||||
emg = u'添加失败, 此IDC %s 已存在!' % idc_name
|
||||
return my_render('jasset/idc_add.html', locals(), request)
|
||||
else:
|
||||
idc_form.save()
|
||||
smg = u'IDC: %s添加成功' % idc_name
|
||||
return HttpResponseRedirect(reverse('idc_list'))
|
||||
else:
|
||||
idc_form = IdcForm()
|
||||
return my_render('jasset/idc_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def idc_list(request):
|
||||
"""
|
||||
IDC list view
|
||||
"""
|
||||
header_title, path1, path2 = u'查看IDC', u'资产管理', u'查看IDC'
|
||||
posts = IDC.objects.all()
|
||||
keyword = request.GET.get('keyword', '')
|
||||
if keyword:
|
||||
posts = IDC.objects.filter(Q(name__contains=keyword) | Q(comment__contains=keyword))
|
||||
else:
|
||||
posts = IDC.objects.exclude(name='ALL').order_by('id')
|
||||
contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request)
|
||||
return my_render('jasset/idc_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def idc_edit(request):
|
||||
"""
|
||||
IDC edit view
|
||||
"""
|
||||
header_title, path1, path2 = u'编辑IDC', u'资产管理', u'编辑IDC'
|
||||
idc_id = request.GET.get('id', '')
|
||||
idc = get_object(IDC, id=idc_id)
|
||||
if request.method == 'POST':
|
||||
idc_form = IdcForm(request.POST, instance=idc)
|
||||
if idc_form.is_valid():
|
||||
idc_form.save()
|
||||
return HttpResponseRedirect(reverse('idc_list'))
|
||||
else:
|
||||
idc_form = IdcForm(instance=idc)
|
||||
return my_render('jasset/idc_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def idc_del(request):
|
||||
"""
|
||||
IDC delete view
|
||||
"""
|
||||
idc_ids = request.GET.get('id', '')
|
||||
idc_id_list = idc_ids.split(',')
|
||||
|
||||
for idc_id in idc_id_list:
|
||||
IDC.objects.filter(id=idc_id).delete()
|
||||
|
||||
return HttpResponseRedirect(reverse('idc_list'))
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def asset_upload(request):
|
||||
"""
|
||||
Upload asset excel file view
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
excel_file = request.FILES.get('file_name', '')
|
||||
ret = excel_to_db(excel_file)
|
||||
if ret:
|
||||
smg = u'批量添加成功'
|
||||
else:
|
||||
emg = u'批量添加失败,请检查格式.'
|
||||
return my_render('jasset/asset_add_batch.html', locals(), request)
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
121
jlog/log_api.py
|
@ -1,121 +0,0 @@
|
|||
# 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)
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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)
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
11
jlog/urls.py
|
@ -1,11 +0,0 @@
|
|||
# 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'),
|
||||
)
|
379
jlog/views.py
|
@ -1,379 +0,0 @@
|
|||
# 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
|
|
@ -1,12 +0,0 @@
|
|||
# Jperm App
|
||||
|
||||
---
|
||||
|
||||
### 模块 ansible_api
|
||||
|
||||
> 使用说明
|
||||
|
||||
+ 依赖rpm安装包: ansible、 sshpass
|
||||
+ 依赖pip安装包: passlib
|
||||
+ 关于ansible配置: 需要启用配置文件(/etc/ansible/ansible.cfg)的 host_key_checking = False
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,523 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
from tempfile import NamedTemporaryFile
|
||||
import os.path
|
||||
|
||||
from ansible.inventory.group import Group
|
||||
from ansible.inventory.host import Host
|
||||
from ansible.inventory import Inventory
|
||||
from ansible.runner import Runner
|
||||
from ansible.playbook import PlayBook
|
||||
from ansible import callbacks
|
||||
from ansible import utils
|
||||
import ansible.constants as C
|
||||
from passlib.hash import sha512_crypt
|
||||
from django.template.loader import get_template
|
||||
from django.template import Context
|
||||
|
||||
|
||||
from jumpserver.api import logger
|
||||
|
||||
|
||||
API_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ANSIBLE_DIR = os.path.join(API_DIR, 'playbooks')
|
||||
C.HOST_KEY_CHECKING = False
|
||||
|
||||
|
||||
class AnsibleError(StandardError):
|
||||
"""
|
||||
the base AnsibleError which contains error(required),
|
||||
data(optional) and message(optional).
|
||||
存储所有Ansible 异常对象
|
||||
"""
|
||||
def __init__(self, error, data='', message=''):
|
||||
super(AnsibleError, self).__init__(message)
|
||||
self.error = error
|
||||
self.data = data
|
||||
self.message = message
|
||||
|
||||
|
||||
class CommandValueError(AnsibleError):
|
||||
"""
|
||||
indicate the input value has error or invalid.
|
||||
the data specifies the error field of input form.
|
||||
输入不合法 异常对象
|
||||
"""
|
||||
def __init__(self, field, message=''):
|
||||
super(CommandValueError, self).__init__('value:invalid', field, message)
|
||||
|
||||
|
||||
class MyInventory(Inventory):
|
||||
"""
|
||||
this is my ansible inventory object.
|
||||
"""
|
||||
def __init__(self, resource):
|
||||
"""
|
||||
resource的数据格式是一个列表字典,比如
|
||||
{
|
||||
"group1": {
|
||||
"hosts": [{"hostname": "10.10.10.10", "port": "22", "username": "test", "password": "mypass"}, ...],
|
||||
"vars": {"var1": value1, "var2": value2, ...}
|
||||
}
|
||||
}
|
||||
|
||||
如果你只传入1个列表,这默认该列表内的所有主机属于my_group组,比如
|
||||
[{"hostname": "10.10.10.10", "port": "22", "username": "test", "password": "mypass"}, ...]
|
||||
"""
|
||||
self.resource = resource
|
||||
self.inventory = Inventory(host_list=[])
|
||||
self.gen_inventory()
|
||||
|
||||
def my_add_group(self, hosts, groupname, groupvars=None):
|
||||
"""
|
||||
add hosts to a group
|
||||
"""
|
||||
my_group = Group(name=groupname)
|
||||
|
||||
# if group variables exists, add them to group
|
||||
if groupvars:
|
||||
for key, value in groupvars.iteritems():
|
||||
my_group.set_variable(key, value)
|
||||
|
||||
# add hosts to group
|
||||
for host in hosts:
|
||||
# set connection variables
|
||||
hostname = host.get("hostname")
|
||||
hostip = host.get('ip', hostname)
|
||||
hostport = host.get("port")
|
||||
username = host.get("username")
|
||||
password = host.get("password")
|
||||
ssh_key = host.get("ssh_key")
|
||||
my_host = Host(name=hostname, port=hostport)
|
||||
my_host.set_variable('ansible_ssh_host', hostip)
|
||||
my_host.set_variable('ansible_ssh_port', hostport)
|
||||
my_host.set_variable('ansible_ssh_user', username)
|
||||
my_host.set_variable('ansible_ssh_pass', password)
|
||||
my_host.set_variable('ansible_ssh_private_key_file', ssh_key)
|
||||
|
||||
# set other variables
|
||||
for key, value in host.iteritems():
|
||||
if key not in ["hostname", "port", "username", "password"]:
|
||||
my_host.set_variable(key, value)
|
||||
# add to group
|
||||
my_group.add_host(my_host)
|
||||
|
||||
self.inventory.add_group(my_group)
|
||||
|
||||
def gen_inventory(self):
|
||||
"""
|
||||
add hosts to inventory.
|
||||
"""
|
||||
if isinstance(self.resource, list):
|
||||
self.my_add_group(self.resource, 'default_group')
|
||||
elif isinstance(self.resource, dict):
|
||||
for groupname, hosts_and_vars in self.resource.iteritems():
|
||||
self.my_add_group(hosts_and_vars.get("hosts"), groupname, hosts_and_vars.get("vars"))
|
||||
|
||||
|
||||
class MyRunner(MyInventory):
|
||||
"""
|
||||
This is a General object for parallel execute modules.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyRunner, self).__init__(*args, **kwargs)
|
||||
self.results_raw = {}
|
||||
|
||||
def run(self, module_name='shell', module_args='', timeout=10, forks=10, pattern='*',
|
||||
become=False, become_method='sudo', become_user='root', become_pass='', transport='paramiko'):
|
||||
"""
|
||||
run module from andible ad-hoc.
|
||||
module_name: ansible module_name
|
||||
module_args: ansible module args
|
||||
"""
|
||||
hoc = Runner(module_name=module_name,
|
||||
module_args=module_args,
|
||||
timeout=timeout,
|
||||
inventory=self.inventory,
|
||||
pattern=pattern,
|
||||
forks=forks,
|
||||
become=become,
|
||||
become_method=become_method,
|
||||
become_user=become_user,
|
||||
become_pass=become_pass,
|
||||
transport=transport
|
||||
)
|
||||
self.results_raw = hoc.run()
|
||||
logger.debug(self.results_raw)
|
||||
return self.results_raw
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
"""
|
||||
{'failed': {'localhost': ''}, 'ok': {'jumpserver': ''}}
|
||||
"""
|
||||
result = {'failed': {}, 'ok': {}}
|
||||
dark = self.results_raw.get('dark')
|
||||
contacted = self.results_raw.get('contacted')
|
||||
if dark:
|
||||
for host, info in dark.items():
|
||||
result['failed'][host] = info.get('msg')
|
||||
|
||||
if contacted:
|
||||
for host, info in contacted.items():
|
||||
if info.get('invocation').get('module_name') in ['raw', 'shell', 'command', 'script']:
|
||||
if info.get('rc') == 0:
|
||||
result['ok'][host] = info.get('stdout') + info.get('stderr')
|
||||
else:
|
||||
result['failed'][host] = info.get('stdout') + info.get('stderr')
|
||||
else:
|
||||
if info.get('failed'):
|
||||
result['failed'][host] = info.get('msg')
|
||||
else:
|
||||
result['ok'][host] = info.get('changed')
|
||||
return result
|
||||
|
||||
|
||||
class Command(MyInventory):
|
||||
"""
|
||||
this is a command object for parallel execute command.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Command, self).__init__(*args, **kwargs)
|
||||
self.results_raw = {}
|
||||
|
||||
def run(self, command, module_name="command", timeout=10, forks=10, pattern=''):
|
||||
"""
|
||||
run command from andible ad-hoc.
|
||||
command : 必须是一个需要执行的命令字符串, 比如
|
||||
'uname -a'
|
||||
"""
|
||||
data = {}
|
||||
|
||||
if module_name not in ["raw", "command", "shell"]:
|
||||
raise CommandValueError("module_name",
|
||||
"module_name must be of the 'raw, command, shell'")
|
||||
hoc = Runner(module_name=module_name,
|
||||
module_args=command,
|
||||
timeout=timeout,
|
||||
inventory=self.inventory,
|
||||
pattern=pattern,
|
||||
forks=forks,
|
||||
)
|
||||
self.results_raw = hoc.run()
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
result = {}
|
||||
for k, v in self.results_raw.items():
|
||||
if k == 'dark':
|
||||
for host, info in v.items():
|
||||
result[host] = {'dark': info.get('msg')}
|
||||
elif k == 'contacted':
|
||||
for host, info in v.items():
|
||||
result[host] = {}
|
||||
if info.get('stdout'):
|
||||
result[host]['stdout'] = info.get('stdout')
|
||||
elif info.get('stderr'):
|
||||
result[host]['stderr'] = info.get('stderr')
|
||||
return result
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
result = {}
|
||||
if self.stdout:
|
||||
result['ok'] = self.stdout
|
||||
if self.stderr:
|
||||
result['err'] = self.stderr
|
||||
if self.dark:
|
||||
result['dark'] = self.dark
|
||||
return result
|
||||
|
||||
@property
|
||||
def exec_time(self):
|
||||
"""
|
||||
get the command execute time.
|
||||
"""
|
||||
result = {}
|
||||
all = self.results_raw.get("contacted")
|
||||
for key, value in all.iteritems():
|
||||
result[key] = {
|
||||
"start": value.get("start"),
|
||||
"end" : value.get("end"),
|
||||
"delta": value.get("delta"),}
|
||||
return result
|
||||
|
||||
@property
|
||||
def stdout(self):
|
||||
"""
|
||||
get the comamnd standard output.
|
||||
"""
|
||||
result = {}
|
||||
all = self.results_raw.get("contacted")
|
||||
for key, value in all.iteritems():
|
||||
result[key] = value.get("stdout")
|
||||
return result
|
||||
|
||||
@property
|
||||
def stderr(self):
|
||||
"""
|
||||
get the command standard error.
|
||||
"""
|
||||
result = {}
|
||||
all = self.results_raw.get("contacted")
|
||||
for key, value in all.iteritems():
|
||||
if value.get("stderr") or value.get("warnings"):
|
||||
result[key] = {
|
||||
"stderr": value.get("stderr"),
|
||||
"warnings": value.get("warnings"),}
|
||||
return result
|
||||
|
||||
@property
|
||||
def dark(self):
|
||||
"""
|
||||
get the dark results.
|
||||
"""
|
||||
return self.results_raw.get("dark")
|
||||
|
||||
|
||||
class MyTask(MyRunner):
|
||||
"""
|
||||
this is a tasks object for include the common command.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyTask, self).__init__(*args, **kwargs)
|
||||
|
||||
def push_key(self, user, key_path):
|
||||
"""
|
||||
push the ssh authorized key to target.
|
||||
"""
|
||||
module_args = 'user="%s" key="{{ lookup("file", "%s") }}" state=present' % (user, key_path)
|
||||
self.run("authorized_key", module_args, become=True)
|
||||
|
||||
return self.results
|
||||
|
||||
def push_multi_key(self, **user_info):
|
||||
"""
|
||||
push multi key
|
||||
:param user_info:
|
||||
:return:
|
||||
"""
|
||||
ret_failed = []
|
||||
ret_success = []
|
||||
for user, key_path in user_info.iteritems():
|
||||
ret = self.push_key(user, key_path)
|
||||
if ret.get("status") == "ok":
|
||||
ret_success.append(ret)
|
||||
if ret.get("status") == "failed":
|
||||
ret_failed.append(ret)
|
||||
|
||||
if ret_failed:
|
||||
return {"status": "failed", "msg": ret_failed}
|
||||
else:
|
||||
return {"status": "success", "msg": ret_success}
|
||||
|
||||
def del_key(self, user, key_path):
|
||||
"""
|
||||
push the ssh authorized key to target.
|
||||
"""
|
||||
if user == 'root':
|
||||
return {"status": "failed", "msg": "root cann't be delete"}
|
||||
module_args = 'user="%s" key="{{ lookup("file", "%s") }}" state="absent"' % (user, key_path)
|
||||
self.run("authorized_key", module_args, become=True)
|
||||
|
||||
return self.results
|
||||
|
||||
def add_user(self, username, password=''):
|
||||
"""
|
||||
add a host user.
|
||||
"""
|
||||
|
||||
if password:
|
||||
encrypt_pass = sha512_crypt.encrypt(password)
|
||||
module_args = 'name=%s shell=/bin/bash password=%s' % (username, encrypt_pass)
|
||||
else:
|
||||
module_args = 'name=%s shell=/bin/bash' % username
|
||||
|
||||
self.run("user", module_args, become=True)
|
||||
|
||||
return self.results
|
||||
|
||||
def add_multi_user(self, **user_info):
|
||||
"""
|
||||
add multi user
|
||||
:param user_info: keyword args
|
||||
{username: password}
|
||||
:return:
|
||||
"""
|
||||
ret_success = []
|
||||
ret_failed = []
|
||||
for user, password in user_info.iteritems():
|
||||
ret = self.add_user(user, password)
|
||||
if ret.get("status") == "ok":
|
||||
ret_success.append(ret)
|
||||
if ret.get("status") == "failed":
|
||||
ret_failed.append(ret)
|
||||
|
||||
if ret_failed:
|
||||
return {"status": "failed", "msg": ret_failed}
|
||||
else:
|
||||
return {"status": "success", "msg": ret_success}
|
||||
|
||||
def del_user(self, username):
|
||||
"""
|
||||
delete a host user.
|
||||
"""
|
||||
if username == 'root':
|
||||
return {"status": "failed", "msg": "root cann't be delete"}
|
||||
module_args = 'name=%s state=absent remove=yes move_home=yes force=yes' % username
|
||||
self.run("user", module_args, become=True)
|
||||
return self.results
|
||||
|
||||
def del_user_sudo(self, username):
|
||||
"""
|
||||
delete a role sudo item
|
||||
:param username:
|
||||
:return:
|
||||
"""
|
||||
if username == 'root':
|
||||
return {"status": "failed", "msg": "root cann't be delete"}
|
||||
module_args = "sed -i 's/^%s.*//' /etc/sudoers" % username
|
||||
self.run("command", module_args, become=True)
|
||||
return self.results
|
||||
|
||||
@staticmethod
|
||||
def gen_sudo_script(role_list, sudo_list):
|
||||
# receive role_list = [role1, role2] sudo_list = [sudo1, sudo2]
|
||||
# return sudo_alias={'NETWORK': '/sbin/ifconfig, /ls'} sudo_user={'user1': ['NETWORK', 'SYSTEM']}
|
||||
sudo_alias = {}
|
||||
sudo_user = {}
|
||||
for sudo in sudo_list:
|
||||
sudo_alias[sudo.name] = sudo.commands
|
||||
|
||||
for role in role_list:
|
||||
sudo_user[role.name] = ','.join(sudo_alias.keys())
|
||||
|
||||
sudo_j2 = get_template('jperm/role_sudo.j2')
|
||||
sudo_content = sudo_j2.render(Context({"sudo_alias": sudo_alias, "sudo_user": sudo_user}))
|
||||
sudo_file = NamedTemporaryFile(delete=False)
|
||||
sudo_file.write(sudo_content)
|
||||
sudo_file.close()
|
||||
return sudo_file.name
|
||||
|
||||
def push_sudo_file(self, role_list, sudo_list):
|
||||
"""
|
||||
use template to render pushed sudoers file
|
||||
:return:
|
||||
"""
|
||||
module_args1 = self.gen_sudo_script(role_list, sudo_list)
|
||||
self.run("script", module_args1, become=True)
|
||||
return self.results
|
||||
|
||||
|
||||
class CustomAggregateStats(callbacks.AggregateStats):
|
||||
"""
|
||||
Holds stats about per-host activity during playbook runs.
|
||||
"""
|
||||
def __init__(self):
|
||||
super(CustomAggregateStats, self).__init__()
|
||||
self.results = []
|
||||
|
||||
def compute(self, runner_results, setup=False, poll=False,
|
||||
ignore_errors=False):
|
||||
"""
|
||||
Walk through all results and increment stats.
|
||||
"""
|
||||
super(CustomAggregateStats, self).compute(runner_results, setup, poll,
|
||||
ignore_errors)
|
||||
|
||||
self.results.append(runner_results)
|
||||
|
||||
def summarize(self, host):
|
||||
"""
|
||||
Return information about a particular host
|
||||
"""
|
||||
summarized_info = super(CustomAggregateStats, self).summarize(host)
|
||||
|
||||
# Adding the info I need
|
||||
summarized_info['result'] = self.results
|
||||
|
||||
return summarized_info
|
||||
|
||||
|
||||
class MyPlaybook(MyInventory):
|
||||
"""
|
||||
this is my playbook object for execute playbook.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyPlaybook, self).__init__(*args, **kwargs)
|
||||
|
||||
def run(self, playbook_relational_path, extra_vars=None):
|
||||
"""
|
||||
run ansible playbook,
|
||||
only surport relational path.
|
||||
"""
|
||||
stats = callbacks.AggregateStats()
|
||||
playbook_cb = callbacks.PlaybookCallbacks(verbose=utils.VERBOSITY)
|
||||
runner_cb = callbacks.PlaybookRunnerCallbacks(stats, verbose=utils.VERBOSITY)
|
||||
playbook_path = os.path.join(ANSIBLE_DIR, playbook_relational_path)
|
||||
|
||||
pb = PlayBook(
|
||||
playbook=playbook_path,
|
||||
stats=stats,
|
||||
callbacks=playbook_cb,
|
||||
runner_callbacks=runner_cb,
|
||||
inventory=self.inventory,
|
||||
extra_vars=extra_vars,
|
||||
check=False)
|
||||
|
||||
self.results = pb.run()
|
||||
|
||||
@property
|
||||
def raw_results(self):
|
||||
"""
|
||||
get the raw results after playbook run.
|
||||
"""
|
||||
return self.results
|
||||
|
||||
|
||||
class App(MyPlaybook):
|
||||
"""
|
||||
this is a app object for inclue the common playbook.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(App, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# resource = {
|
||||
# "group1": {
|
||||
# "hosts": [{"hostname": "127.0.0.1", "port": "22", "username": "root", "password": "xxx"},],
|
||||
# "vars" : {"var1": "value1", "var2": "value2"},
|
||||
# },
|
||||
# }
|
||||
|
||||
resource = [{"hostname": "127.0.0.1", "port": "22", "username": "yumaojun", "password": "yusky0902",
|
||||
# "ansible_become": "yes",
|
||||
# "ansible_become_method": "sudo",
|
||||
# # "ansible_become_user": "root",
|
||||
# "ansible_become_pass": "yusky0902",
|
||||
}]
|
||||
cmd.run('ls',pattern='*')
|
||||
print cmd.results_raw
|
||||
|
||||
# resource = [{"hostname": "192.168.10.148", "port": "22", "username": "root", "password": "xxx"}]
|
||||
# task = Tasks(resource)
|
||||
# print task.get_host_info()
|
||||
|
||||
# playbook = MyPlaybook(resource)
|
||||
# playbook.run('test.yml')
|
||||
# print playbook.raw_results
|
||||
|
||||
# task = Tasks(resource)
|
||||
# print task.add_user('test', 'mypass')
|
||||
# print task.del_user('test')
|
||||
# print task.push_key('root', '/root/.ssh/id_rsa.pub')
|
||||
# print task.del_key('root', '/root/.ssh/id_rsa.pub')
|
||||
|
||||
# task = Tasks(resource)
|
||||
# print task.add_init_users()
|
||||
# print task.del_init_users()
|
||||
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
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)
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
from django.db.models.query import QuerySet
|
||||
from jumpserver.api import *
|
||||
import uuid
|
||||
import re
|
||||
|
||||
from jumpserver.models import Setting
|
||||
from jperm.models import PermRole, PermPush, PermRule
|
||||
|
||||
|
||||
def get_group_user_perm(ob):
|
||||
"""
|
||||
ob为用户或用户组
|
||||
获取用户、用户组授权的资产、资产组
|
||||
return:
|
||||
{’asset_group': {
|
||||
asset_group1: {'asset': [], 'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
asset_group2: {'asset: [], 'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
}
|
||||
'asset':{
|
||||
asset1: {'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
asset2: {'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
}
|
||||
]},
|
||||
'rule':[rule1, rule2,]
|
||||
'role': {role1: {'asset': []}, 'asset_group': []}, role2: {}},
|
||||
}
|
||||
"""
|
||||
perm = {}
|
||||
if isinstance(ob, User):
|
||||
rule_all = set(PermRule.objects.filter(user=ob))
|
||||
for user_group in ob.group.all():
|
||||
rule_all = rule_all.union(set(PermRule.objects.filter(user_group=user_group)))
|
||||
|
||||
elif isinstance(ob, UserGroup):
|
||||
rule_all = PermRule.objects.filter(user_group=ob)
|
||||
else:
|
||||
rule_all = []
|
||||
|
||||
perm['rule'] = rule_all
|
||||
perm_asset_group = perm['asset_group'] = {}
|
||||
perm_asset = perm['asset'] = {}
|
||||
perm_role = perm['role'] = {}
|
||||
for rule in rule_all:
|
||||
asset_groups = rule.asset_group.all()
|
||||
assets = rule.asset.all()
|
||||
perm_roles = rule.role.all()
|
||||
group_assets = []
|
||||
for asset_group in asset_groups:
|
||||
group_assets.extend(asset_group.asset_set.all())
|
||||
# 获取一个规则授权的角色和对应主机
|
||||
for role in perm_roles:
|
||||
if perm_role.get(role):
|
||||
perm_role[role]['asset'] = perm_role[role].get('asset', set()).union(set(assets).union(set(group_assets)))
|
||||
perm_role[role]['asset_group'] = perm_role[role].get('asset_group', set()).union(set(asset_groups))
|
||||
else:
|
||||
perm_role[role] = {'asset': set(assets).union(set(group_assets)), 'asset_group': set(asset_groups)}
|
||||
|
||||
# 获取一个规则用户授权的资产
|
||||
for asset in assets:
|
||||
if perm_asset.get(asset):
|
||||
perm_asset[asset].get('role', set()).update(set(rule.role.all()))
|
||||
perm_asset[asset].get('rule', set()).add(rule)
|
||||
else:
|
||||
perm_asset[asset] = {'role': set(rule.role.all()), 'rule': set([rule])}
|
||||
|
||||
# 获取一个规则用户授权的资产组
|
||||
for asset_group in asset_groups:
|
||||
asset_group_assets = asset_group.asset_set.all()
|
||||
if perm_asset_group.get(asset_group):
|
||||
perm_asset_group[asset_group].get('role', set()).update(set(rule.role.all()))
|
||||
perm_asset_group[asset_group].get('rule', set()).add(rule)
|
||||
else:
|
||||
perm_asset_group[asset_group] = {'role': set(rule.role.all()), 'rule': set([rule]),
|
||||
'asset': asset_group_assets}
|
||||
|
||||
# 将资产组中的资产添加到资产授权中
|
||||
for asset in asset_group_assets:
|
||||
if perm_asset.get(asset):
|
||||
perm_asset[asset].get('role', set()).update(perm_asset_group[asset_group].get('role', set()))
|
||||
perm_asset[asset].get('rule', set()).update(perm_asset_group[asset_group].get('rule', set()))
|
||||
else:
|
||||
perm_asset[asset] = {'role': perm_asset_group[asset_group].get('role', set()),
|
||||
'rule': perm_asset_group[asset_group].get('rule', set())}
|
||||
return perm
|
||||
|
||||
|
||||
def get_group_asset_perm(ob):
|
||||
"""
|
||||
ob为资产或资产组
|
||||
获取资产,资产组授权的用户,用户组
|
||||
return:
|
||||
{’user_group': {
|
||||
user_group1: {'user': [], 'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
user_group2: {'user: [], 'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
}
|
||||
'user':{
|
||||
user1: {'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
user2: {'role': [role1, role2], 'rule': [rule1, rule2]},
|
||||
}
|
||||
]},
|
||||
'rule':[rule1, rule2,],
|
||||
}
|
||||
"""
|
||||
perm = {}
|
||||
if isinstance(ob, Asset):
|
||||
rule_all = PermRule.objects.filter(asset=ob)
|
||||
elif isinstance(ob, AssetGroup):
|
||||
rule_all = PermRule.objects.filter(asset_group=ob)
|
||||
else:
|
||||
rule_all = []
|
||||
|
||||
perm['rule'] = rule_all
|
||||
perm_user_group = perm['user_group'] = {}
|
||||
perm_user = perm['user'] = {}
|
||||
for rule in rule_all:
|
||||
user_groups = rule.user_group.all()
|
||||
users = rule.user.all()
|
||||
# 获取一个规则资产的用户
|
||||
for user in users:
|
||||
if perm_user.get(user):
|
||||
perm_user[user].get('role', set()).update(set(rule.role.all()))
|
||||
perm_user[user].get('rule', set()).add(rule)
|
||||
else:
|
||||
perm_user[user] = {'role': set(rule.role.all()), 'rule': set([rule])}
|
||||
|
||||
# 获取一个规则资产授权的用户组
|
||||
for user_group in user_groups:
|
||||
user_group_users = user_group.user_set.all()
|
||||
if perm_user_group.get(user_group):
|
||||
perm_user_group[user_group].get('role', set()).update(set(rule.role.all()))
|
||||
perm_user_group[user_group].get('rule', set()).add(rule)
|
||||
else:
|
||||
perm_user_group[user_group] = {'role': set(rule.role.all()), 'rule': set([rule]),
|
||||
'user': user_group_users}
|
||||
|
||||
# 将用户组中的资产添加到用户授权中
|
||||
for user in user_group_users:
|
||||
if perm_user.get(user):
|
||||
perm_user[user].get('role', set()).update(perm_user_group[user_group].get('role', set()))
|
||||
perm_user[user].get('rule', set()).update(perm_user_group[user_group].get('rule', set()))
|
||||
else:
|
||||
perm_user[user] = {'role': perm_user_group[user_group].get('role', set()),
|
||||
'rule': perm_user_group[user_group].get('rule', set())}
|
||||
return perm
|
||||
|
||||
|
||||
def user_have_perm(user, asset):
|
||||
user_perm_all = get_group_user_perm(user)
|
||||
user_assets = user_perm_all.get('asset').keys()
|
||||
if asset in user_assets:
|
||||
return user_perm_all.get('asset').get(asset).get('role')
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def gen_resource(ob, perm=None):
|
||||
"""
|
||||
ob为用户或资产列表或资产queryset, 如果同时输入用户和{'role': role1, 'asset': []},则获取用户在这些资产上的信息
|
||||
生成MyInventory需要的 resource文件
|
||||
"""
|
||||
res = []
|
||||
if isinstance(ob, dict):
|
||||
role = ob.get('role')
|
||||
asset_r = ob.get('asset')
|
||||
user = ob.get('user')
|
||||
if not perm:
|
||||
perm = get_group_user_perm(user)
|
||||
|
||||
if role:
|
||||
roles = perm.get('role', {}).keys() # 获取用户所有授权角色
|
||||
if role not in roles:
|
||||
return {}
|
||||
|
||||
role_assets_all = perm.get('role').get(role).get('asset') # 获取用户该角色所有授权主机
|
||||
assets = set(role_assets_all) & set(asset_r) # 获取用户提交中合法的主机
|
||||
|
||||
for asset in assets:
|
||||
asset_info = get_asset_info(asset)
|
||||
role_key = get_role_key(user, role)
|
||||
info = {'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'port': asset_info.get('port', 22),
|
||||
'ansible_ssh_private_key_file': role_key,
|
||||
'username': role.name,
|
||||
# 'password': CRYPTOR.decrypt(role.password)
|
||||
}
|
||||
|
||||
if os.path.isfile(role_key):
|
||||
info['ssh_key'] = role_key
|
||||
|
||||
res.append(info)
|
||||
else:
|
||||
for asset, asset_info in perm.get('asset').items():
|
||||
if asset not in asset_r:
|
||||
continue
|
||||
asset_info = get_asset_info(asset)
|
||||
try:
|
||||
role = sorted(list(perm.get('asset').get(asset).get('role')))[0]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
role_key = get_role_key(user, role)
|
||||
info = {'hostname': asset.hostname,
|
||||
'ip': asset.ip,
|
||||
'port': asset_info.get('port', 22),
|
||||
'username': role.name,
|
||||
'password': CRYPTOR.decrypt(role.password),
|
||||
}
|
||||
if os.path.isfile(role_key):
|
||||
info['ssh_key'] = role_key
|
||||
|
||||
res.append(info)
|
||||
|
||||
elif isinstance(ob, User):
|
||||
if not perm:
|
||||
perm = get_group_user_perm(ob)
|
||||
|
||||
for asset, asset_info in perm.get('asset').items():
|
||||
asset_info = get_asset_info(asset)
|
||||
info = {'hostname': asset.hostname, 'ip': asset.ip, 'port': asset_info.get('port', 22)}
|
||||
try:
|
||||
role = sorted(list(perm.get('asset').get(asset).get('role')))[0]
|
||||
except IndexError:
|
||||
continue
|
||||
info['username'] = role.name
|
||||
info['password'] = CRYPTOR.decrypt(role.password)
|
||||
|
||||
role_key = get_role_key(ob, role)
|
||||
if os.path.isfile(role_key):
|
||||
info['ssh_key'] = role_key
|
||||
res.append(info)
|
||||
|
||||
elif isinstance(ob, (list, QuerySet)):
|
||||
for asset in ob:
|
||||
info = get_asset_info(asset)
|
||||
res.append(info)
|
||||
logger.debug('生成res: %s' % res)
|
||||
return res
|
||||
|
||||
|
||||
def get_object_list(model, id_list):
|
||||
"""根据id列表获取对象列表"""
|
||||
object_list = []
|
||||
for object_id in id_list:
|
||||
if object_id:
|
||||
object_list.extend(model.objects.filter(id=int(object_id)))
|
||||
|
||||
return object_list
|
||||
|
||||
|
||||
def get_role_info(role_id, type="all"):
|
||||
"""
|
||||
获取role对应的一些信息
|
||||
:return: 返回值 均为对象列表
|
||||
"""
|
||||
# 获取role对应的授权规则
|
||||
role_obj = PermRole.objects.get(id=role_id)
|
||||
rule_push_obj = role_obj.perm_rule.all()
|
||||
# 获取role 对应的用户 和 用户组
|
||||
# 获取role 对应的主机 和主机组
|
||||
users_obj = []
|
||||
assets_obj = []
|
||||
user_groups_obj = []
|
||||
asset_groups_obj = []
|
||||
for push in rule_push_obj:
|
||||
for user in push.user.all():
|
||||
users_obj.append(user)
|
||||
for asset in push.asset.all():
|
||||
assets_obj.append(asset)
|
||||
for user_group in push.user_group.all():
|
||||
user_groups_obj.append(user_group)
|
||||
for asset_group in push.asset_group.all():
|
||||
asset_groups_obj.append(asset_group)
|
||||
|
||||
if type == "all":
|
||||
return {"rules": set(rule_push_obj),
|
||||
"users": set(users_obj),
|
||||
"user_groups": set(user_groups_obj),
|
||||
"assets": set(assets_obj),
|
||||
"asset_groups": set(asset_groups_obj),
|
||||
}
|
||||
|
||||
elif type == "rule":
|
||||
return set(rule_push_obj)
|
||||
elif type == "user":
|
||||
return set(users_obj)
|
||||
elif type == "user_group":
|
||||
return set(user_groups_obj)
|
||||
elif type == "asset":
|
||||
return set(assets_obj)
|
||||
elif type == "asset_group":
|
||||
return set(asset_groups_obj)
|
||||
else:
|
||||
return u"不支持的查询"
|
||||
|
||||
|
||||
def get_role_push_host(role):
|
||||
"""
|
||||
asset_pushed: {'success': push.success, 'key': push.is_public_key, 'password': push.is_password,
|
||||
'result': push.result}
|
||||
asset_no_push: set(asset1, asset2)
|
||||
"""
|
||||
# 计算该role 所有push记录 总共推送的主机
|
||||
pushs = PermPush.objects.filter(role=role)
|
||||
asset_all = Asset.objects.all()
|
||||
asset_pushed = {}
|
||||
for push in pushs:
|
||||
asset_pushed[push.asset] = {'success': push.success, 'key': push.is_public_key, 'password': push.is_password,
|
||||
'result': push.result}
|
||||
asset_no_push = set(asset_all) - set(asset_pushed.keys())
|
||||
return asset_pushed, asset_no_push
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print get_role_info(1)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
---
|
||||
|
||||
- hosts: 'add_users_group'
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: add SA user
|
||||
command: uname -a
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
---
|
||||
|
||||
- hosts: test
|
||||
gather_facts: no
|
||||
tasks:
|
||||
- name: just for test
|
||||
command: uname -a
|
||||
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,22 +0,0 @@
|
|||
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'),
|
||||
)
|
|
@ -1,80 +0,0 @@
|
|||
# -*- 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()
|
||||
|
||||
|
746
jperm/views.py
|
@ -1,746 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Q
|
||||
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
|
||||
from paramiko import SSHException
|
||||
from jperm.perm_api import *
|
||||
|
||||
from juser.models import User, UserGroup
|
||||
from jasset.models import Asset, AssetGroup
|
||||
from jperm.models import PermRole, PermRule, PermSudo, PermPush
|
||||
from jumpserver.models import Setting
|
||||
|
||||
from jperm.utils import gen_keys, trans_all
|
||||
from jperm.ansible_api import MyTask
|
||||
from jperm.perm_api import get_role_info, get_role_push_host
|
||||
from jumpserver.api import my_render, get_object, CRYPTOR
|
||||
|
||||
|
||||
# 设置PERM APP Log
|
||||
from jumpserver.api import logger
|
||||
#logger = set_log(LOG_LEVEL, filename='jumpserver_perm.log')
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_rule_list(request):
|
||||
"""
|
||||
list rule page
|
||||
授权规则列表
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "授权规则", "规则管理", "查看规则"
|
||||
# 获取所有规则
|
||||
rules_list = PermRule.objects.all()
|
||||
rule_id = request.GET.get('id')
|
||||
# TODO: 搜索和分页
|
||||
keyword = request.GET.get('search', '')
|
||||
if rule_id:
|
||||
rules_list = rules_list.filter(id=rule_id)
|
||||
|
||||
if keyword:
|
||||
rules_list = rules_list.filter(Q(name__icontains=keyword))
|
||||
|
||||
rules_list, p, rules, page_range, current_page, show_first, show_end = pages(rules_list, request)
|
||||
|
||||
return my_render('jperm/perm_rule_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_rule_detail(request):
|
||||
"""
|
||||
rule detail page
|
||||
授权详情
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "授权规则", "规则管理", "规则详情"
|
||||
|
||||
# 根据rule_id 取得rule对象
|
||||
try:
|
||||
if request.method == "GET":
|
||||
rule_id = request.GET.get("id")
|
||||
if not rule_id:
|
||||
raise ServerError("Rule Detail - no rule id get")
|
||||
rule_obj = PermRule.objects.get(id=rule_id)
|
||||
user_obj = rule_obj.user.all()
|
||||
user_group_obj = rule_obj.user_group.all()
|
||||
asset_obj = rule_obj.asset.all()
|
||||
asset_group_obj = rule_obj.asset_group.all()
|
||||
roles_name = [role.name for role in rule_obj.role.all()]
|
||||
|
||||
# 渲染数据
|
||||
roles_name = ','.join(roles_name)
|
||||
rule = rule_obj
|
||||
users = user_obj
|
||||
user_groups = user_group_obj
|
||||
assets = asset_obj
|
||||
asset_groups = asset_group_obj
|
||||
except ServerError, e:
|
||||
logger.warning(e)
|
||||
|
||||
return my_render('jperm/perm_rule_detail.html', locals(), request)
|
||||
|
||||
|
||||
def perm_rule_add(request):
|
||||
"""
|
||||
add rule page
|
||||
添加授权
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "授权规则", "规则管理", "添加规则"
|
||||
|
||||
# 渲染数据, 获取所有 用户,用户组,资产,资产组,用户角色, 用于添加授权规则
|
||||
users = User.objects.all()
|
||||
user_groups = UserGroup.objects.all()
|
||||
assets = Asset.objects.all()
|
||||
asset_groups = AssetGroup.objects.all()
|
||||
roles = PermRole.objects.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
# 获取用户选择的 用户,用户组,资产,资产组,用户角色
|
||||
users_select = request.POST.getlist('user', []) # 需要授权用户
|
||||
user_groups_select = request.POST.getlist('user_group', []) # 需要授权用户组
|
||||
assets_select = request.POST.getlist('asset', []) # 需要授权资产
|
||||
asset_groups_select = request.POST.getlist('asset_group', []) # 需要授权资产组
|
||||
roles_select = request.POST.getlist('role', []) # 需要授权角色
|
||||
rule_name = request.POST.get('name')
|
||||
rule_comment = request.POST.get('comment')
|
||||
|
||||
try:
|
||||
rule = get_object(PermRule, name=rule_name)
|
||||
|
||||
if rule:
|
||||
raise ServerError(u'授权规则 %s 已存在' % rule_name)
|
||||
|
||||
if not rule_name or not roles_select:
|
||||
raise ServerError(u'系统用户名称和规则名称不能为空')
|
||||
|
||||
# 获取需要授权的主机列表
|
||||
assets_obj = [Asset.objects.get(id=asset_id) for asset_id in assets_select]
|
||||
asset_groups_obj = [AssetGroup.objects.get(id=group_id) for group_id in asset_groups_select]
|
||||
group_assets_obj = []
|
||||
for asset_group in asset_groups_obj:
|
||||
group_assets_obj.extend(list(asset_group.asset_set.all()))
|
||||
calc_assets = set(group_assets_obj) | set(assets_obj) # 授权资产和资产组包含的资产
|
||||
|
||||
# 获取需要授权的用户列表
|
||||
users_obj = [User.objects.get(id=user_id) for user_id in users_select]
|
||||
user_groups_obj = [UserGroup.objects.get(id=group_id) for group_id in user_groups_select]
|
||||
|
||||
# 获取授予的角色列表
|
||||
roles_obj = [PermRole.objects.get(id=role_id) for role_id in roles_select]
|
||||
need_push_asset = set()
|
||||
|
||||
for role in roles_obj:
|
||||
asset_no_push = get_role_push_host(role=role)[1] # 获取某角色已经推送的资产
|
||||
need_push_asset.update(set(calc_assets) & set(asset_no_push))
|
||||
if need_push_asset:
|
||||
raise ServerError(u'没有推送系统用户 %s 的主机 %s'
|
||||
% (role.name, ','.join([asset.hostname for asset in need_push_asset])))
|
||||
|
||||
# 仅授权成功的,写回数据库(授权规则,用户,用户组,资产,资产组,用户角色)
|
||||
rule = PermRule(name=rule_name, comment=rule_comment)
|
||||
rule.save()
|
||||
rule.user = users_obj
|
||||
rule.user_group = user_groups_obj
|
||||
rule.asset = assets_obj
|
||||
rule.asset_group = asset_groups_obj
|
||||
rule.role = roles_obj
|
||||
rule.save()
|
||||
|
||||
msg = u"添加授权规则:%s" % rule.name
|
||||
return HttpResponseRedirect(reverse('rule_list'))
|
||||
except ServerError, e:
|
||||
error = e
|
||||
return my_render('jperm/perm_rule_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_rule_edit(request):
|
||||
"""
|
||||
edit rule page
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "授权规则", "规则管理", "添加规则"
|
||||
|
||||
# 根据rule_id 取得rule对象
|
||||
rule_id = request.GET.get("id")
|
||||
rule = get_object(PermRule, id=rule_id)
|
||||
|
||||
# 渲染数据, 获取所选的rule对象
|
||||
|
||||
users = User.objects.all()
|
||||
user_groups = UserGroup.objects.all()
|
||||
assets = Asset.objects.all()
|
||||
asset_groups = AssetGroup.objects.all()
|
||||
roles = PermRole.objects.all()
|
||||
|
||||
if request.method == 'POST' and rule_id:
|
||||
# 获取用户选择的 用户,用户组,资产,资产组,用户角色
|
||||
rule_name = request.POST.get('name')
|
||||
rule_comment = request.POST.get("comment")
|
||||
users_select = request.POST.getlist('user', [])
|
||||
user_groups_select = request.POST.getlist('user_group', [])
|
||||
assets_select = request.POST.getlist('asset', [])
|
||||
asset_groups_select = request.POST.getlist('asset_group', [])
|
||||
roles_select = request.POST.getlist('role', [])
|
||||
|
||||
try:
|
||||
if not rule_name or not roles_select:
|
||||
raise ServerError(u'系统用户和关联系统用户不能为空')
|
||||
|
||||
assets_obj = [Asset.objects.get(id=asset_id) for asset_id in assets_select]
|
||||
asset_groups_obj = [AssetGroup.objects.get(id=group_id) for group_id in asset_groups_select]
|
||||
group_assets_obj = []
|
||||
for asset_group in asset_groups_obj:
|
||||
group_assets_obj.extend(list(asset_group.asset_set.all()))
|
||||
calc_assets = set(group_assets_obj) | set(assets_obj) # 授权资产和资产组包含的资产
|
||||
|
||||
# 获取需要授权的用户列表
|
||||
users_obj = [User.objects.get(id=user_id) for user_id in users_select]
|
||||
user_groups_obj = [UserGroup.objects.get(id=group_id) for group_id in user_groups_select]
|
||||
|
||||
# 获取授予的角色列表
|
||||
roles_obj = [PermRole.objects.get(id=role_id) for role_id in roles_select]
|
||||
need_push_asset = set()
|
||||
for role in roles_obj:
|
||||
asset_no_push = get_role_push_host(role=role)[1] # 获取某角色已经推送的资产
|
||||
need_push_asset.update(set(calc_assets) & set(asset_no_push))
|
||||
if need_push_asset:
|
||||
raise ServerError(u'没有推送系统用户 %s 的主机 %s'
|
||||
% (role.name, ','.join([asset.hostname for asset in need_push_asset])))
|
||||
|
||||
# 仅授权成功的,写回数据库(授权规则,用户,用户组,资产,资产组,用户角色)
|
||||
rule.user = users_obj
|
||||
rule.user_group = user_groups_obj
|
||||
rule.asset = assets_obj
|
||||
rule.asset_group = asset_groups_obj
|
||||
rule.role = roles_obj
|
||||
rule.name = rule_name
|
||||
rule.comment = rule_comment
|
||||
rule.save()
|
||||
msg = u"更新授权规则:%s成功" % rule.name
|
||||
|
||||
except ServerError, e:
|
||||
error = e
|
||||
|
||||
return my_render('jperm/perm_rule_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_rule_delete(request):
|
||||
"""
|
||||
use to delete rule
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
# 根据rule_id 取得rule对象
|
||||
rule_id = request.POST.get("id")
|
||||
rule_obj = PermRule.objects.get(id=rule_id)
|
||||
rule_obj.delete()
|
||||
return HttpResponse(u"删除授权规则:%s" % rule_obj.name)
|
||||
else:
|
||||
return HttpResponse(u"不支持该操作")
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_list(request):
|
||||
"""
|
||||
list role page
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "系统用户", "系统用户管理", "查看系统用户"
|
||||
|
||||
# 获取所有系统角色
|
||||
roles_list = PermRole.objects.all()
|
||||
role_id = request.GET.get('id')
|
||||
# TODO: 搜索和分页
|
||||
keyword = request.GET.get('search', '')
|
||||
if keyword:
|
||||
roles_list = roles_list.filter(Q(name=keyword))
|
||||
|
||||
if role_id:
|
||||
roles_list = roles_list.filter(id=role_id)
|
||||
|
||||
roles_list, p, roles, page_range, current_page, show_first, show_end = pages(roles_list, request)
|
||||
|
||||
return my_render('jperm/perm_role_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_add(request):
|
||||
"""
|
||||
add role page
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "系统用户", "系统用户管理", "添加系统用户"
|
||||
sudos = PermSudo.objects.all()
|
||||
|
||||
if request.method == "POST":
|
||||
# 获取参数: name, comment
|
||||
name = request.POST.get("role_name", "").strip()
|
||||
comment = request.POST.get("role_comment", "")
|
||||
password = request.POST.get("role_password", "")
|
||||
key_content = request.POST.get("role_key", "")
|
||||
sudo_ids = request.POST.getlist('sudo_name')
|
||||
|
||||
try:
|
||||
if get_object(PermRole, name=name):
|
||||
raise ServerError(u'已经存在该用户 %s' % name)
|
||||
if name == "root":
|
||||
raise ServerError(u'禁止使用root用户作为系统用户,这样非常危险!')
|
||||
default = get_object(Setting, name='default')
|
||||
if len(password) > 64:
|
||||
raise ServerError(u'密码长度不能超过64位!')
|
||||
|
||||
if password:
|
||||
encrypt_pass = CRYPTOR.encrypt(password)
|
||||
else:
|
||||
encrypt_pass = CRYPTOR.encrypt(CRYPTOR.gen_rand_pass(20))
|
||||
# 生成随机密码,生成秘钥对
|
||||
sudos_obj = [get_object(PermSudo, id=sudo_id) for sudo_id in sudo_ids]
|
||||
if key_content:
|
||||
try:
|
||||
key_path = gen_keys(key=key_content)
|
||||
except SSHException, e:
|
||||
raise ServerError(e)
|
||||
else:
|
||||
key_path = gen_keys()
|
||||
logger.debug('generate role key: %s' % key_path)
|
||||
role = PermRole(name=name, comment=comment, password=encrypt_pass, key_path=key_path)
|
||||
role.save()
|
||||
role.sudo = sudos_obj
|
||||
msg = u"添加系统用户: %s" % name
|
||||
return HttpResponseRedirect(reverse('role_list'))
|
||||
except ServerError, e:
|
||||
error = e
|
||||
return my_render('jperm/perm_role_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_delete(request):
|
||||
"""
|
||||
delete role page
|
||||
"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
# 获取参数删除的role对象
|
||||
role_id = request.GET.get("id")
|
||||
role = get_object(PermRole, id=role_id)
|
||||
if not role:
|
||||
logger.warning(u"Delete Role: role_id %s not exist" % role_id)
|
||||
raise ServerError(u"role_id %s 无数据记录" % role_id)
|
||||
# 删除推送到主机上的role
|
||||
filter_type = request.GET.get("filter_type")
|
||||
if filter_type:
|
||||
if filter_type == "recycle_assets":
|
||||
recycle_assets = [push.asset for push in role.perm_push.all() if push.success]
|
||||
print recycle_assets
|
||||
recycle_assets_ip = ','.join([asset.ip for asset in recycle_assets])
|
||||
return HttpResponse(recycle_assets_ip)
|
||||
else:
|
||||
return HttpResponse("no such filter_type: %s" % filter_type)
|
||||
else:
|
||||
return HttpResponse("filter_type: ?")
|
||||
except ServerError, e:
|
||||
return HttpResponse(e)
|
||||
if request.method == "POST":
|
||||
try:
|
||||
# 获取参数删除的role对象
|
||||
role_id = request.POST.get("id")
|
||||
role = get_object(PermRole, id=role_id)
|
||||
if not role:
|
||||
logger.warning(u"Delete Role: role_id %s not exist" % role_id)
|
||||
raise ServerError(u"role_id %s 无数据记录" % role_id)
|
||||
role_key = role.key_path
|
||||
# 删除推送到主机上的role
|
||||
recycle_assets = [push.asset for push in role.perm_push.all() if push.success]
|
||||
logger.debug(u"delete role %s - delete_assets: %s" % (role.name, recycle_assets))
|
||||
if recycle_assets:
|
||||
recycle_resource = gen_resource(recycle_assets)
|
||||
task = MyTask(recycle_resource)
|
||||
try:
|
||||
msg_del_user = task.del_user(get_object(PermRole, id=role_id).name)
|
||||
msg_del_sudo = task.del_user_sudo(get_object(PermRole, id=role_id).name)
|
||||
except Exception, e:
|
||||
logger.warning(u"Recycle Role failed: %s" % e)
|
||||
raise ServerError(u"回收已推送的系统用户失败: %s" % e)
|
||||
logger.info(u"delete role %s - execute delete user: %s" % (role.name, msg_del_user))
|
||||
logger.info(u"delete role %s - execute delete sudo: %s" % (role.name, msg_del_sudo))
|
||||
# TODO: 判断返回结果,处理异常
|
||||
# 删除存储的秘钥,以及目录
|
||||
try:
|
||||
key_files = os.listdir(role_key)
|
||||
for key_file in key_files:
|
||||
os.remove(os.path.join(role_key, key_file))
|
||||
os.rmdir(role_key)
|
||||
except OSError, e:
|
||||
logger.warning(u"Delete Role: delete key error, %s" % e)
|
||||
raise ServerError(u"删除系统用户key失败: %s" % e)
|
||||
logger.info(u"delete role %s - delete role key directory: %s" % (role.name, role_key))
|
||||
# 数据库里删除记录
|
||||
role.delete()
|
||||
return HttpResponse(u"删除系统用户: %s" % role.name)
|
||||
except ServerError, e:
|
||||
return HttpResponseBadRequest(u"删除失败, 原因: %s" % e)
|
||||
return HttpResponseNotAllowed(u"仅支持POST")
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_detail(request):
|
||||
"""
|
||||
the role detail page
|
||||
the role_info data like:
|
||||
{'asset_groups': [],
|
||||
'assets': [<Asset: 192.168.10.148>],
|
||||
'rules': [<PermRule: PermRule object>],
|
||||
'': [],
|
||||
'': [<User: user1>]}
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户详情"
|
||||
|
||||
try:
|
||||
if request.method == "GET":
|
||||
role_id = request.GET.get("id")
|
||||
if not role_id:
|
||||
raise ServerError("not role id")
|
||||
role = get_object(PermRole, id=role_id)
|
||||
role_info = get_role_info(role_id)
|
||||
|
||||
# 渲染数据
|
||||
rules = role_info.get("rules")
|
||||
assets = role_info.get("assets")
|
||||
asset_groups = role_info.get("asset_groups")
|
||||
users = role_info.get("users")
|
||||
user_groups = role_info.get("user_groups")
|
||||
pushed_asset, need_push_asset = get_role_push_host(get_object(PermRole, id=role_id))
|
||||
|
||||
except ServerError, e:
|
||||
logger.warning(e)
|
||||
|
||||
return my_render('jperm/perm_role_detail.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_edit(request):
|
||||
"""
|
||||
edit role page
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户编辑"
|
||||
|
||||
# 渲染数据
|
||||
role_id = request.GET.get("id")
|
||||
role = PermRole.objects.get(id=role_id)
|
||||
role_pass = CRYPTOR.decrypt(role.password)
|
||||
sudo_all = PermSudo.objects.all()
|
||||
role_sudos = role.sudo.all()
|
||||
sudo_all = PermSudo.objects.all()
|
||||
if request.method == "GET":
|
||||
return my_render('jperm/perm_role_edit.html', locals(), request)
|
||||
|
||||
if request.method == "POST":
|
||||
# 获取 POST 数据
|
||||
role_name = request.POST.get("role_name")
|
||||
role_password = request.POST.get("role_password")
|
||||
role_comment = request.POST.get("role_comment")
|
||||
role_sudo_names = request.POST.getlist("sudo_name")
|
||||
role_sudos = [PermSudo.objects.get(id=sudo_id) for sudo_id in role_sudo_names]
|
||||
key_content = request.POST.get("role_key", "")
|
||||
if len(role_password) > 64:
|
||||
raise ServerError(u'密码长度不能超过64位!')
|
||||
|
||||
try:
|
||||
if not role:
|
||||
raise ServerError('该系统用户不能存在')
|
||||
|
||||
if role_name == "root":
|
||||
raise ServerError(u'禁止使用root用户作为系统用户,这样非常危险!')
|
||||
|
||||
if role_password:
|
||||
encrypt_pass = CRYPTOR.encrypt(role_password)
|
||||
role.password = encrypt_pass
|
||||
# 生成随机密码,生成秘钥对
|
||||
if key_content:
|
||||
try:
|
||||
key_path = gen_keys(key=key_content, key_path_dir=role.key_path)
|
||||
except SSHException:
|
||||
raise ServerError('输入的密钥不合法')
|
||||
logger.debug('Recreate role key: %s' % role.key_path)
|
||||
# 写入数据库
|
||||
role.name = role_name
|
||||
role.comment = role_comment
|
||||
role.sudo = role_sudos
|
||||
|
||||
role.save()
|
||||
msg = u"更新系统用户: %s" % role.name
|
||||
return HttpResponseRedirect(reverse('role_list'))
|
||||
except ServerError, e:
|
||||
error = e
|
||||
|
||||
return my_render('jperm/perm_role_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_push(request):
|
||||
"""
|
||||
the role push page
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户推送"
|
||||
role_id = request.GET.get('id')
|
||||
asset_ids = request.GET.get('asset_id')
|
||||
role = get_object(PermRole, id=role_id)
|
||||
assets = Asset.objects.all()
|
||||
asset_groups = AssetGroup.objects.all()
|
||||
if asset_ids:
|
||||
need_push_asset = [get_object(Asset, id=asset_id) for asset_id in asset_ids.split(',')]
|
||||
|
||||
if request.method == "POST":
|
||||
# 获取推荐角色的名称列表
|
||||
# 计算出需要推送的资产列表
|
||||
asset_ids = request.POST.getlist("assets")
|
||||
asset_group_ids = request.POST.getlist("asset_groups")
|
||||
assets_obj = [Asset.objects.get(id=asset_id) for asset_id in asset_ids]
|
||||
asset_groups_obj = [AssetGroup.objects.get(id=asset_group_id) for asset_group_id in asset_group_ids]
|
||||
group_assets_obj = []
|
||||
for asset_group in asset_groups_obj:
|
||||
group_assets_obj.extend(asset_group.asset_set.all())
|
||||
calc_assets = list(set(assets_obj) | set(group_assets_obj))
|
||||
|
||||
push_resource = gen_resource(calc_assets)
|
||||
|
||||
# 调用Ansible API 进行推送
|
||||
password_push = True if request.POST.get("use_password") else False
|
||||
key_push = True if request.POST.get("use_publicKey") else False
|
||||
task = MyTask(push_resource)
|
||||
ret = {}
|
||||
|
||||
# 因为要先建立用户,而push key是在 password也完成的情况下的 可选项
|
||||
# 1. 以秘钥 方式推送角色
|
||||
if key_push:
|
||||
ret["pass_push"] = task.add_user(role.name)
|
||||
ret["key_push"] = task.push_key(role.name, os.path.join(role.key_path, 'id_rsa.pub'))
|
||||
|
||||
# 2. 推送账号密码 <为了安全 系统用户统一使用秘钥进行通信, 不再提供密码方式的推送>
|
||||
# elif password_push:
|
||||
# ret["pass_push"] = task.add_user(role.name, CRYPTOR.decrypt(role.password))
|
||||
|
||||
# 3. 推送sudo配置文件
|
||||
if key_push:
|
||||
sudo_list = set([sudo for sudo in role.sudo.all()]) # set(sudo1, sudo2, sudo3)
|
||||
if sudo_list:
|
||||
ret['sudo'] = task.push_sudo_file([role], sudo_list)
|
||||
|
||||
logger.debug('推送role结果: %s' % ret)
|
||||
success_asset = {}
|
||||
failed_asset = {}
|
||||
logger.debug(ret)
|
||||
for push_type, result in ret.items():
|
||||
if result.get('failed'):
|
||||
for hostname, info in result.get('failed').items():
|
||||
if hostname in failed_asset.keys():
|
||||
if info in failed_asset.get(hostname):
|
||||
failed_asset[hostname] += info
|
||||
else:
|
||||
failed_asset[hostname] = info
|
||||
|
||||
for push_type, result in ret.items():
|
||||
if result.get('ok'):
|
||||
for hostname, info in result.get('ok').items():
|
||||
if hostname in failed_asset.keys():
|
||||
continue
|
||||
elif hostname in success_asset.keys():
|
||||
if str(info) in success_asset.get(hostname, ''):
|
||||
success_asset[hostname] += str(info)
|
||||
else:
|
||||
success_asset[hostname] = str(info)
|
||||
|
||||
# 推送成功 回写push表
|
||||
for asset in calc_assets:
|
||||
push_check = PermPush.objects.filter(role=role, asset=asset)
|
||||
if push_check:
|
||||
func = push_check.update
|
||||
else:
|
||||
def func(**kwargs):
|
||||
PermPush(**kwargs).save()
|
||||
|
||||
if failed_asset.get(asset.hostname):
|
||||
func(is_password=password_push, is_public_key=key_push, role=role, asset=asset, success=False,
|
||||
result=failed_asset.get(asset.hostname))
|
||||
else:
|
||||
func(is_password=password_push, is_public_key=key_push, role=role, asset=asset, success=True)
|
||||
|
||||
if not failed_asset:
|
||||
msg = u'系统用户 %s 推送成功[ %s ]' % (role.name, ','.join(success_asset.keys()))
|
||||
else:
|
||||
error = u'系统用户 %s 推送失败 [ %s ], 推送成功 [ %s ] 进入系统用户详情,查看失败原因' % (role.name,
|
||||
','.join(failed_asset.keys()),
|
||||
','.join(success_asset.keys()))
|
||||
return my_render('jperm/perm_role_push.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_sudo_list(request):
|
||||
"""
|
||||
list sudo commands alias
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "Sudo命令", "别名管理", "查看别名"
|
||||
|
||||
# 获取所有sudo 命令别名
|
||||
sudos_list = PermSudo.objects.all()
|
||||
|
||||
# TODO: 搜索和分页
|
||||
keyword = request.GET.get('search', '')
|
||||
if keyword:
|
||||
sudos_list = sudos_list.filter(Q(name=keyword))
|
||||
|
||||
sudos_list, p, sudos, page_range, current_page, show_first, show_end = pages(sudos_list, request)
|
||||
|
||||
return my_render('jperm/perm_sudo_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_sudo_add(request):
|
||||
"""
|
||||
list sudo commands alias
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "Sudo命令", "别名管理", "添加别名"
|
||||
try:
|
||||
if request.method == "POST":
|
||||
# 获取参数: name, comment
|
||||
name = request.POST.get("sudo_name").strip().upper()
|
||||
comment = request.POST.get("sudo_comment").strip()
|
||||
commands = request.POST.get("sudo_commands").strip()
|
||||
|
||||
if not name or not commands:
|
||||
raise ServerError(u"sudo name 和 commands是必填项!")
|
||||
|
||||
pattern = re.compile(r'[\n,\r]')
|
||||
deal_space_commands = list_drop_str(pattern.split(commands), u'')
|
||||
deal_all_commands = map(trans_all, deal_space_commands)
|
||||
commands = ', '.join(deal_all_commands)
|
||||
logger.debug(u'添加sudo %s: %s' % (name, commands))
|
||||
|
||||
if get_object(PermSudo, name=name):
|
||||
error = 'Sudo别名 %s已经存在' % name
|
||||
else:
|
||||
sudo = PermSudo(name=name.strip(), comment=comment, commands=commands)
|
||||
sudo.save()
|
||||
msg = u"添加Sudo命令别名: %s" % name
|
||||
except ServerError, e:
|
||||
error = e
|
||||
return my_render('jperm/perm_sudo_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_sudo_edit(request):
|
||||
"""
|
||||
list sudo commands alias
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
# 渲染数据
|
||||
header_title, path1, path2 = "Sudo命令", "别名管理", "编辑别名"
|
||||
|
||||
sudo_id = request.GET.get("id")
|
||||
sudo = PermSudo.objects.get(id=sudo_id)
|
||||
|
||||
try:
|
||||
if request.method == "POST":
|
||||
name = request.POST.get("sudo_name").upper()
|
||||
commands = request.POST.get("sudo_commands")
|
||||
comment = request.POST.get("sudo_comment")
|
||||
|
||||
if not name or not commands:
|
||||
raise ServerError(u"sudo name 和 commands是必填项!")
|
||||
|
||||
pattern = re.compile(r'[\n,\r]')
|
||||
deal_space_commands = list_drop_str(pattern.split(commands), u'')
|
||||
deal_all_commands = map(trans_all, deal_space_commands)
|
||||
commands = ', '.join(deal_all_commands).strip()
|
||||
logger.debug(u'添加sudo %s: %s' % (name, commands))
|
||||
|
||||
sudo.name = name.strip()
|
||||
sudo.commands = commands
|
||||
sudo.comment = comment
|
||||
sudo.save()
|
||||
|
||||
msg = u"更新命令别名: %s" % name
|
||||
except ServerError, e:
|
||||
error = e
|
||||
return my_render('jperm/perm_sudo_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_sudo_delete(request):
|
||||
"""
|
||||
list sudo commands alias
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
if request.method == "POST":
|
||||
# 获取参数删除的role对象
|
||||
sudo_id = request.POST.get("id")
|
||||
sudo = PermSudo.objects.get(id=sudo_id)
|
||||
# 数据库里删除记录
|
||||
sudo.delete()
|
||||
return HttpResponse(u"删除系统用户: %s" % sudo.name)
|
||||
else:
|
||||
return HttpResponse(u"不支持该操作")
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def perm_role_recycle(request):
|
||||
role_id = request.GET.get('role_id')
|
||||
asset_ids = request.GET.get('asset_id').split(',')
|
||||
|
||||
# 仅有推送的角色才回收
|
||||
assets = [get_object(Asset, id=asset_id) for asset_id in asset_ids]
|
||||
recycle_assets = []
|
||||
for asset in assets:
|
||||
if True in [push.success for push in asset.perm_push.all()]:
|
||||
recycle_assets.append(asset)
|
||||
recycle_resource = gen_resource(recycle_assets)
|
||||
task = MyTask(recycle_resource)
|
||||
try:
|
||||
msg_del_user = task.del_user(get_object(PermRole, id=role_id).name)
|
||||
msg_del_sudo = task.del_user_sudo(get_object(PermRole, id=role_id).name)
|
||||
logger.info("recycle user msg: %s" % msg_del_user)
|
||||
logger.info("recycle sudo msg: %s" % msg_del_sudo)
|
||||
except Exception, e:
|
||||
logger.warning("Recycle Role failed: %s" % e)
|
||||
raise ServerError(u"回收已推送的系统用户失败: %s" % e)
|
||||
|
||||
for asset_id in asset_ids:
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
assets.append(asset)
|
||||
role = get_object(PermRole, id=role_id)
|
||||
PermPush.objects.filter(asset=asset, role=role).delete()
|
||||
|
||||
return HttpResponse('删除成功')
|
||||
|
||||
|
||||
@require_role('user')
|
||||
def perm_role_get(request):
|
||||
asset_id = request.GET.get('id', 0)
|
||||
if asset_id:
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
role = user_have_perm(request.user, asset=asset)
|
||||
logger.debug(u'获取授权系统用户: ' + ','.join([i.name for i in role]))
|
||||
return HttpResponse(','.join([i.name for i in role]))
|
||||
else:
|
||||
roles = get_group_user_perm(request.user).get('role').keys()
|
||||
return HttpResponse(','.join(i.name for i in roles))
|
||||
|
||||
return HttpResponse('error')
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
[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
|
|
@ -1,510 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
import os, sys, time, re
|
||||
from Crypto.Cipher import AES
|
||||
import crypt
|
||||
import pwd
|
||||
from binascii import b2a_hex, a2b_hex
|
||||
import hashlib
|
||||
import datetime
|
||||
import random
|
||||
import subprocess
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
|
||||
from settings import *
|
||||
from django.core.paginator import Paginator, EmptyPage, InvalidPage
|
||||
from django.http import HttpResponse, Http404
|
||||
from django.template import RequestContext
|
||||
from juser.models import User, UserGroup
|
||||
from jlog.models import Log, TtyLog
|
||||
from jasset.models import Asset, AssetGroup
|
||||
from jperm.models import PermRule, PermRole
|
||||
from jumpserver.models import Setting
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import render_to_response
|
||||
from django.core.mail import send_mail
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
|
||||
def set_log(level, filename='jumpserver.log'):
|
||||
"""
|
||||
return a log file object
|
||||
根据提示设置log打印
|
||||
"""
|
||||
log_file = os.path.join(LOG_DIR, filename)
|
||||
if not os.path.isfile(log_file):
|
||||
os.mknod(log_file)
|
||||
os.chmod(log_file, 0777)
|
||||
log_level_total = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARN, 'error': logging.ERROR,
|
||||
'critical': logging.CRITICAL}
|
||||
logger_f = logging.getLogger('jumpserver')
|
||||
logger_f.setLevel(logging.DEBUG)
|
||||
fh = logging.FileHandler(log_file)
|
||||
fh.setLevel(log_level_total.get(level, logging.DEBUG))
|
||||
formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s')
|
||||
fh.setFormatter(formatter)
|
||||
logger_f.addHandler(fh)
|
||||
return logger_f
|
||||
|
||||
|
||||
def list_drop_str(a_list, a_str):
|
||||
for i in a_list:
|
||||
if i == a_str:
|
||||
a_list.remove(a_str)
|
||||
return a_list
|
||||
|
||||
|
||||
def get_asset_info(asset):
|
||||
"""
|
||||
获取资产的相关管理账号端口等信息
|
||||
"""
|
||||
default = get_object(Setting, name='default')
|
||||
info = {'hostname': asset.hostname, 'ip': asset.ip}
|
||||
if asset.use_default_auth:
|
||||
if default:
|
||||
info['username'] = default.field1
|
||||
try:
|
||||
info['password'] = CRYPTOR.decrypt(default.field3)
|
||||
except ServerError:
|
||||
pass
|
||||
if os.path.isfile(default.field4):
|
||||
info['ssh_key'] = default.field4
|
||||
else:
|
||||
info['username'] = asset.username
|
||||
info['password'] = CRYPTOR.decrypt(asset.password)
|
||||
try:
|
||||
info['port'] = int(asset.port)
|
||||
except TypeError:
|
||||
info['port'] = int(default.field2)
|
||||
|
||||
return info
|
||||
|
||||
|
||||
def get_role_key(user, role):
|
||||
"""
|
||||
由于role的key的权限是所有人可以读的, ansible执行命令等要求为600,所以拷贝一份到特殊目录
|
||||
:param user:
|
||||
:param role:
|
||||
:return: self key path
|
||||
"""
|
||||
user_role_key_dir = os.path.join(KEY_DIR, 'user')
|
||||
user_role_key_path = os.path.join(user_role_key_dir, '%s_%s.pem' % (user.username, role.name))
|
||||
mkdir(user_role_key_dir, mode=777)
|
||||
if not os.path.isfile(user_role_key_path):
|
||||
with open(os.path.join(role.key_path, 'id_rsa')) as fk:
|
||||
with open(user_role_key_path, 'w') as fu:
|
||||
fu.write(fk.read())
|
||||
logger.debug(u"创建新的系统用户key %s, Owner: %s" % (user_role_key_path, user.username))
|
||||
chown(user_role_key_path, user.username)
|
||||
os.chmod(user_role_key_path, 0600)
|
||||
return user_role_key_path
|
||||
|
||||
|
||||
def chown(path, user, group=''):
|
||||
if not group:
|
||||
group = user
|
||||
try:
|
||||
uid = pwd.getpwnam(user).pw_uid
|
||||
gid = pwd.getpwnam(group).pw_gid
|
||||
os.chown(path, uid, gid)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
def page_list_return(total, current=1):
|
||||
"""
|
||||
page
|
||||
分页,返回本次分页的最小页数到最大页数列表
|
||||
"""
|
||||
min_page = current - 2 if current - 4 > 0 else 1
|
||||
max_page = min_page + 4 if min_page + 4 < total else total
|
||||
|
||||
return range(min_page, max_page + 1)
|
||||
|
||||
|
||||
def pages(post_objects, request):
|
||||
"""
|
||||
page public function , return page's object tuple
|
||||
分页公用函数,返回分页的对象元组
|
||||
"""
|
||||
paginator = Paginator(post_objects, 20)
|
||||
try:
|
||||
current_page = int(request.GET.get('page', '1'))
|
||||
except ValueError:
|
||||
current_page = 1
|
||||
|
||||
page_range = page_list_return(len(paginator.page_range), current_page)
|
||||
|
||||
try:
|
||||
page_objects = paginator.page(current_page)
|
||||
except (EmptyPage, InvalidPage):
|
||||
page_objects = paginator.page(paginator.num_pages)
|
||||
|
||||
if current_page >= 5:
|
||||
show_first = 1
|
||||
else:
|
||||
show_first = 0
|
||||
|
||||
if current_page <= (len(paginator.page_range) - 3):
|
||||
show_end = 1
|
||||
else:
|
||||
show_end = 0
|
||||
|
||||
# 所有对象, 分页器, 本页对象, 所有页码, 本页页码,是否显示第一页,是否显示最后一页
|
||||
return post_objects, paginator, page_objects, page_range, current_page, show_first, show_end
|
||||
|
||||
|
||||
class PyCrypt(object):
|
||||
"""
|
||||
This class used to encrypt and decrypt password.
|
||||
加密类
|
||||
"""
|
||||
|
||||
def __init__(self, key):
|
||||
self.key = key
|
||||
self.mode = AES.MODE_CBC
|
||||
|
||||
@staticmethod
|
||||
def gen_rand_pass(length=16, especial=False):
|
||||
"""
|
||||
random password
|
||||
随机生成密码
|
||||
"""
|
||||
salt_key = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_'
|
||||
symbol = '!@$%^&*()_'
|
||||
salt_list = []
|
||||
if especial:
|
||||
for i in range(length - 4):
|
||||
salt_list.append(random.choice(salt_key))
|
||||
for i in range(4):
|
||||
salt_list.append(random.choice(symbol))
|
||||
else:
|
||||
for i in range(length):
|
||||
salt_list.append(random.choice(salt_key))
|
||||
salt = ''.join(salt_list)
|
||||
return salt
|
||||
|
||||
@staticmethod
|
||||
def md5_crypt(string):
|
||||
"""
|
||||
md5 encrypt method
|
||||
md5非对称加密方法
|
||||
"""
|
||||
return hashlib.new("md5", string).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def gen_sha512(salt, password):
|
||||
"""
|
||||
generate sha512 format password
|
||||
生成sha512加密密码
|
||||
"""
|
||||
return crypt.crypt(password, '$6$%s$' % salt)
|
||||
|
||||
def encrypt(self, passwd=None, length=32):
|
||||
"""
|
||||
encrypt gen password
|
||||
对称加密之加密生成密码
|
||||
"""
|
||||
if not passwd:
|
||||
passwd = self.gen_rand_pass()
|
||||
|
||||
cryptor = AES.new(self.key, self.mode, b'8122ca7d906ad5e1')
|
||||
try:
|
||||
count = len(passwd)
|
||||
except TypeError:
|
||||
raise ServerError('Encrypt password error, TYpe error.')
|
||||
|
||||
add = (length - (count % length))
|
||||
passwd += ('\0' * add)
|
||||
cipher_text = cryptor.encrypt(passwd)
|
||||
return b2a_hex(cipher_text)
|
||||
|
||||
def decrypt(self, text):
|
||||
"""
|
||||
decrypt pass base the same key
|
||||
对称加密之解密,同一个加密随机数
|
||||
"""
|
||||
cryptor = AES.new(self.key, self.mode, b'8122ca7d906ad5e1')
|
||||
try:
|
||||
plain_text = cryptor.decrypt(a2b_hex(text))
|
||||
except TypeError:
|
||||
raise ServerError('Decrypt password error, TYpe error.')
|
||||
return plain_text.rstrip('\0')
|
||||
|
||||
|
||||
class ServerError(Exception):
|
||||
"""
|
||||
self define exception
|
||||
自定义异常
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def get_object(model, **kwargs):
|
||||
"""
|
||||
use this function for query
|
||||
使用改封装函数查询数据库
|
||||
"""
|
||||
for value in kwargs.values():
|
||||
if not value:
|
||||
return None
|
||||
|
||||
the_object = model.objects.filter(**kwargs)
|
||||
if len(the_object) == 1:
|
||||
the_object = the_object[0]
|
||||
else:
|
||||
the_object = None
|
||||
return the_object
|
||||
|
||||
|
||||
def require_role(role='user'):
|
||||
"""
|
||||
decorator for require user role in ["super", "admin", "user"]
|
||||
要求用户是某种角色 ["super", "admin", "user"]的装饰器
|
||||
"""
|
||||
|
||||
def _deco(func):
|
||||
def __deco(request, *args, **kwargs):
|
||||
request.session['pre_url'] = request.path
|
||||
if not request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
if role == 'admin':
|
||||
# if request.session.get('role_id', 0) < 1:
|
||||
if request.user.role == 'CU':
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
elif role == 'super':
|
||||
# if request.session.get('role_id', 0) < 2:
|
||||
if request.user.role in ['CU', 'GA']:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
return func(request, *args, **kwargs)
|
||||
|
||||
return __deco
|
||||
|
||||
return _deco
|
||||
|
||||
|
||||
def is_role_request(request, role='user'):
|
||||
"""
|
||||
require this request of user is right
|
||||
要求请求角色正确
|
||||
"""
|
||||
role_all = {'user': 'CU', 'admin': 'GA', 'super': 'SU'}
|
||||
if request.user.role == role_all.get(role, 'CU'):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_session_user_dept(request):
|
||||
"""
|
||||
get department of the user in session
|
||||
获取session中用户的部门
|
||||
"""
|
||||
# user_id = request.session.get('user_id', 0)
|
||||
# print '#' * 20
|
||||
# print user_id
|
||||
# user = User.objects.filter(id=user_id)
|
||||
# if user:
|
||||
# user = user[0]
|
||||
# return user, None
|
||||
return request.user, None
|
||||
|
||||
|
||||
@require_role
|
||||
def get_session_user_info(request):
|
||||
"""
|
||||
get the user info of the user in session, for example id, username etc.
|
||||
获取用户的信息
|
||||
"""
|
||||
# user_id = request.session.get('user_id', 0)
|
||||
# user = get_object(User, id=user_id)
|
||||
# if user:
|
||||
# return [user.id, user.username, user]
|
||||
return [request.user.id, request.user.username, request.user]
|
||||
|
||||
|
||||
def get_user_dept(request):
|
||||
"""
|
||||
get the user dept id
|
||||
获取用户的部门id
|
||||
"""
|
||||
user_id = request.user.id
|
||||
if user_id:
|
||||
user_dept = User.objects.get(id=user_id).dept
|
||||
return user_dept.id
|
||||
|
||||
|
||||
def api_user(request):
|
||||
hosts = Log.objects.filter(is_finished=0).count()
|
||||
users = Log.objects.filter(is_finished=0).values('user').distinct().count()
|
||||
ret = {'users': users, 'hosts': hosts}
|
||||
json_data = json.dumps(ret)
|
||||
return HttpResponse(json_data)
|
||||
|
||||
|
||||
def view_splitter(request, su=None, adm=None):
|
||||
"""
|
||||
for different user use different view
|
||||
视图分页器
|
||||
"""
|
||||
if is_role_request(request, 'super'):
|
||||
return su(request)
|
||||
elif is_role_request(request, 'admin'):
|
||||
return adm(request)
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('login'))
|
||||
|
||||
|
||||
def validate(request, user_group=None, user=None, asset_group=None, asset=None, edept=None):
|
||||
"""
|
||||
validate the user request
|
||||
判定用户请求是否合法
|
||||
"""
|
||||
dept = get_session_user_dept(request)[1]
|
||||
if edept:
|
||||
if dept.id != int(edept[0]):
|
||||
return False
|
||||
|
||||
if user_group:
|
||||
dept_user_groups = dept.usergroup_set.all()
|
||||
user_group_ids = []
|
||||
for group in dept_user_groups:
|
||||
user_group_ids.append(str(group.id))
|
||||
|
||||
if not set(user_group).issubset(set(user_group_ids)):
|
||||
return False
|
||||
|
||||
if user:
|
||||
dept_users = dept.user_set.all()
|
||||
user_ids = []
|
||||
for dept_user in dept_users:
|
||||
user_ids.append(str(dept_user.id))
|
||||
|
||||
if not set(user).issubset(set(user_ids)):
|
||||
return False
|
||||
|
||||
if asset_group:
|
||||
dept_asset_groups = dept.bisgroup_set.all()
|
||||
asset_group_ids = []
|
||||
for group in dept_asset_groups:
|
||||
asset_group_ids.append(str(group.id))
|
||||
|
||||
if not set(asset_group).issubset(set(asset_group_ids)):
|
||||
return False
|
||||
|
||||
if asset:
|
||||
dept_assets = dept.asset_set.all()
|
||||
asset_ids = []
|
||||
for dept_asset in dept_assets:
|
||||
asset_ids.append(str(dept_asset.id))
|
||||
|
||||
if not set(asset).issubset(set(asset_ids)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verify(request, user_group=None, user=None, asset_group=None, asset=None, edept=None):
|
||||
dept = get_session_user_dept(request)[1]
|
||||
if edept:
|
||||
if dept.id != int(edept[0]):
|
||||
return False
|
||||
|
||||
if user_group:
|
||||
dept_user_groups = dept.usergroup_set.all()
|
||||
user_groups = []
|
||||
for user_group_id in user_group:
|
||||
user_groups.extend(UserGroup.objects.filter(id=user_group_id))
|
||||
if not set(user_groups).issubset(set(dept_user_groups)):
|
||||
return False
|
||||
|
||||
if user:
|
||||
dept_users = dept.user_set.all()
|
||||
users = []
|
||||
for user_id in user:
|
||||
users.extend(User.objects.filter(id=user_id))
|
||||
|
||||
if not set(users).issubset(set(dept_users)):
|
||||
return False
|
||||
|
||||
if asset_group:
|
||||
dept_asset_groups = dept.bisgroup_set.all()
|
||||
asset_group_ids = []
|
||||
for group in dept_asset_groups:
|
||||
asset_group_ids.append(str(group.id))
|
||||
|
||||
if not set(asset_group).issubset(set(asset_group_ids)):
|
||||
return False
|
||||
|
||||
if asset:
|
||||
dept_assets = dept.asset_set.all()
|
||||
asset_ids = []
|
||||
for a in dept_assets:
|
||||
asset_ids.append(str(a.id))
|
||||
print asset, asset_ids
|
||||
if not set(asset).issubset(set(asset_ids)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def bash(cmd):
|
||||
"""
|
||||
run a bash shell command
|
||||
执行bash命令
|
||||
"""
|
||||
return subprocess.call(cmd, shell=True)
|
||||
|
||||
|
||||
def mkdir(dir_name, username='', mode=755):
|
||||
"""
|
||||
insure the dir exist and mode ok
|
||||
目录存在,如果不存在就建立,并且权限正确
|
||||
"""
|
||||
cmd = '[ ! -d %s ] && mkdir -p %s && chmod %s %s' % (dir_name, dir_name, mode, dir_name)
|
||||
bash(cmd)
|
||||
if username:
|
||||
chown(dir_name, username)
|
||||
|
||||
|
||||
def http_success(request, msg):
|
||||
return render_to_response('success.html', locals())
|
||||
|
||||
|
||||
def http_error(request, emg):
|
||||
message = emg
|
||||
return render_to_response('error.html', locals())
|
||||
|
||||
|
||||
def my_render(template, data, request):
|
||||
return render_to_response(template, data, context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def get_tmp_dir():
|
||||
seed = uuid.uuid4().hex[:4]
|
||||
dir_name = os.path.join('/tmp', '%s-%s' % (datetime.datetime.now().strftime('%Y%m%d-%H%M%S'), seed))
|
||||
mkdir(dir_name, mode=777)
|
||||
return dir_name
|
||||
|
||||
|
||||
def defend_attack(func):
|
||||
def _deco(request, *args, **kwargs):
|
||||
if int(request.session.get('visit', 1)) > 10:
|
||||
logger.debug('请求次数: %s' % request.session.get('visit', 1))
|
||||
return HttpResponse('Forbidden', status=403)
|
||||
request.session['visit'] = request.session.get('visit', 1) + 1
|
||||
request.session.set_expiry(300)
|
||||
return func(request, *args, **kwargs)
|
||||
return _deco
|
||||
|
||||
|
||||
def get_mac_address():
|
||||
node = uuid.getnode()
|
||||
mac = uuid.UUID(int=node).hex[-12:]
|
||||
return mac
|
||||
|
||||
|
||||
CRYPTOR = PyCrypt(KEY)
|
||||
logger = set_log(LOG_LEVEL)
|
|
@ -1,25 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# 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
|
|
@ -1,176 +0,0 @@
|
|||
"""
|
||||
Django settings for jumpserver project.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.7/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.7/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
import ConfigParser
|
||||
import getpass
|
||||
|
||||
config = ConfigParser.ConfigParser()
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
|
||||
config.read(os.path.join(BASE_DIR, 'jumpserver.conf'))
|
||||
KEY_DIR = os.path.join(BASE_DIR, 'keys')
|
||||
|
||||
AUTH_USER_MODEL = 'juser.User'
|
||||
# mail config
|
||||
MAIL_ENABLE = config.get('mail', 'mail_enable')
|
||||
EMAIL_HOST = config.get('mail', 'email_host')
|
||||
EMAIL_PORT = config.get('mail', 'email_port')
|
||||
EMAIL_HOST_USER = config.get('mail', 'email_host_user')
|
||||
EMAIL_HOST_PASSWORD = config.get('mail', 'email_host_password')
|
||||
EMAIL_USE_TLS = config.getboolean('mail', 'email_use_tls')
|
||||
try:
|
||||
EMAIL_USE_SSL = config.getboolean('mail', 'email_use_ssl')
|
||||
except ConfigParser.NoOptionError:
|
||||
EMAIL_USE_SSL = False
|
||||
EMAIL_BACKEND = 'django_smtp_ssl.SSLEmailBackend' if EMAIL_USE_SSL else 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_TIMEOUT = 5
|
||||
|
||||
# ======== Log ==========
|
||||
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||||
SSH_KEY_DIR = os.path.join(BASE_DIR, 'keys/role_keys')
|
||||
KEY = config.get('base', 'key')
|
||||
URL = config.get('base', 'url')
|
||||
LOG_LEVEL = config.get('base', 'log')
|
||||
IP = config.get('base', 'ip')
|
||||
PORT = config.get('base', 'port')
|
||||
|
||||
# ======== Connect ==========
|
||||
try:
|
||||
NAV_SORT_BY = config.get('connect', 'nav_sort_by')
|
||||
except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
|
||||
NAV_SORT_BY = 'ip'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '!%=t81uof5rhmtpi&(zr=q^fah#$enny-c@mswz49l42j0o49-'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
TEMPLATE_DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['0.0.0.0/8']
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'django.contrib.humanize',
|
||||
'django_crontab',
|
||||
'bootstrapform',
|
||||
'jumpserver',
|
||||
'juser',
|
||||
'jasset',
|
||||
'jperm',
|
||||
'jlog',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
# 'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
# 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'jumpserver.urls'
|
||||
|
||||
WSGI_APPLICATION = 'jumpserver.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
|
||||
DATABASES = {}
|
||||
if config.get('db', 'engine') == 'mysql':
|
||||
DB_HOST = config.get('db', 'host')
|
||||
DB_PORT = config.getint('db', 'port')
|
||||
DB_USER = config.get('db', 'user')
|
||||
DB_PASSWORD = config.get('db', 'password')
|
||||
DB_DATABASE = config.get('db', 'database')
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': DB_DATABASE,
|
||||
'USER': DB_USER,
|
||||
'PASSWORD': DB_PASSWORD,
|
||||
'HOST': DB_HOST,
|
||||
'PORT': DB_PORT,
|
||||
}
|
||||
}
|
||||
elif config.get('db', 'engine') == 'sqlite':
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': config.get('db', 'database'),
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.core.context_processors.debug',
|
||||
'django.core.context_processors.i18n',
|
||||
'django.core.context_processors.media',
|
||||
'django.core.context_processors.static',
|
||||
'django.core.context_processors.tz',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
'jumpserver.context_processors.name_proc',
|
||||
)
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
)
|
||||
|
||||
# STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, "static"),
|
||||
)
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.7/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'Asia/Shanghai'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = False
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
BOOTSTRAP_COLUMN_COUNT = 10
|
||||
|
||||
CRONJOBS = [
|
||||
('0 1 * * *', 'jasset.asset_api.asset_ansible_update_all'),
|
||||
('*/10 * * * *', 'jlog.log_api.kill_invalid_connection'),
|
||||
]
|
|
@ -1,48 +0,0 @@
|
|||
# -*- 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
|
||||
|
|
@ -1 +0,0 @@
|
|||
__author__ = 'Hudie'
|
|
@ -1,306 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
import re
|
||||
import ast
|
||||
import time
|
||||
|
||||
from django import template
|
||||
from jperm.models import PermPush
|
||||
from jumpserver.api import *
|
||||
from jperm.perm_api import get_group_user_perm
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name='int2str')
|
||||
def int2str(value):
|
||||
"""
|
||||
int 转换为 str
|
||||
"""
|
||||
return str(value)
|
||||
|
||||
|
||||
@register.filter(name='get_role')
|
||||
def get_role(user_id):
|
||||
"""
|
||||
根据用户id获取用户权限
|
||||
"""
|
||||
|
||||
user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'}
|
||||
user = get_object(User, id=user_id)
|
||||
if user:
|
||||
return user_role.get(str(user.role), u"普通用户")
|
||||
else:
|
||||
return u"普通用户"
|
||||
|
||||
|
||||
@register.filter(name='groups2str')
|
||||
def groups2str(group_list):
|
||||
"""
|
||||
将用户组列表转换为str
|
||||
"""
|
||||
if len(group_list) < 3:
|
||||
return ' '.join([group.name for group in group_list])
|
||||
else:
|
||||
return '%s ...' % ' '.join([group.name for group in group_list[0:2]])
|
||||
|
||||
|
||||
@register.filter(name='user_asset_count')
|
||||
def user_asset_count(user):
|
||||
"""
|
||||
返回用户权限主机的数量
|
||||
"""
|
||||
assets = user.asset.all()
|
||||
asset_groups = user.asset_group.all()
|
||||
|
||||
for asset_group in asset_groups:
|
||||
if asset_group:
|
||||
assets.extend(asset_group.asset_set.all())
|
||||
|
||||
return len(assets)
|
||||
|
||||
|
||||
@register.filter(name='user_asset_group_count')
|
||||
def user_asset_group_count(user):
|
||||
"""
|
||||
返回用户权限主机组的数量
|
||||
"""
|
||||
return len(user.asset_group.all())
|
||||
|
||||
|
||||
@register.filter(name='bool2str')
|
||||
def bool2str(value):
|
||||
if value:
|
||||
return u'是'
|
||||
else:
|
||||
return u'否'
|
||||
|
||||
|
||||
@register.filter(name='members_count')
|
||||
def members_count(group_id):
|
||||
"""统计用户组下成员数量"""
|
||||
group = get_object(UserGroup, id=group_id)
|
||||
if group:
|
||||
return group.user_set.count()
|
||||
else:
|
||||
return 0
|
||||
|
||||
|
||||
@register.filter(name='to_name')
|
||||
def to_name(user_id):
|
||||
"""user id 转位用户名称"""
|
||||
try:
|
||||
user = User.objects.filter(id=int(user_id))
|
||||
if user:
|
||||
user = user[0]
|
||||
return user.name
|
||||
except:
|
||||
return '非法用户'
|
||||
|
||||
|
||||
@register.filter(name='to_role_name')
|
||||
def to_role_name(role_id):
|
||||
"""role_id 转变为角色名称"""
|
||||
role_dict = {'0': '普通用户', '1': '组管理员', '2': '超级管理员'}
|
||||
return role_dict.get(str(role_id), '未知')
|
||||
|
||||
|
||||
@register.filter(name='to_avatar')
|
||||
def to_avatar(role_id='0'):
|
||||
"""不同角色不同头像"""
|
||||
role_dict = {'0': 'user', '1': 'admin', '2': 'root'}
|
||||
return role_dict.get(str(role_id), 'user')
|
||||
|
||||
|
||||
@register.filter(name='result2bool')
|
||||
def result2bool(result=''):
|
||||
"""将结果定向为结果"""
|
||||
result = eval(result)
|
||||
unreachable = result.get('unreachable', [])
|
||||
failures = result.get('failures', [])
|
||||
|
||||
if unreachable or failures:
|
||||
return '<b style="color: red">失败</b>'
|
||||
else:
|
||||
return '<b style="color: green">成功</b>'
|
||||
|
||||
|
||||
@register.filter(name='rule_member_count')
|
||||
def rule_member_count(instance, member):
|
||||
"""
|
||||
instance is a rule object,
|
||||
use to get the number of the members
|
||||
:param instance:
|
||||
:param member:
|
||||
:return:
|
||||
"""
|
||||
member = getattr(instance, member)
|
||||
counts = member.all().count()
|
||||
return str(counts)
|
||||
|
||||
|
||||
@register.filter(name='rule_member_name')
|
||||
def rule_member_name(instance, member):
|
||||
"""
|
||||
instance is a rule object,
|
||||
use to get the name of the members
|
||||
:param instance:
|
||||
:param member:
|
||||
:return:
|
||||
"""
|
||||
member = getattr(instance, member)
|
||||
names = member.all()
|
||||
|
||||
return names
|
||||
|
||||
|
||||
@register.filter(name='user_which_groups')
|
||||
def user_which_group(user, member):
|
||||
"""
|
||||
instance is a user object,
|
||||
use to get the group of the user
|
||||
:param instance:
|
||||
:param member:
|
||||
:return:
|
||||
"""
|
||||
member = getattr(user, member)
|
||||
names = [members.name for members in member.all()]
|
||||
|
||||
return ','.join(names)
|
||||
|
||||
|
||||
@register.filter(name='asset_which_groups')
|
||||
def asset_which_group(asset, member):
|
||||
"""
|
||||
instance is a user object,
|
||||
use to get the group of the user
|
||||
:param instance:
|
||||
:param member:
|
||||
:return:
|
||||
"""
|
||||
member = getattr(asset, member)
|
||||
names = [members.name for members in member.all()]
|
||||
|
||||
return ','.join(names)
|
||||
|
||||
|
||||
@register.filter(name='group_str2')
|
||||
def groups_str2(group_list):
|
||||
"""
|
||||
将用户组列表转换为str
|
||||
"""
|
||||
if len(group_list) < 3:
|
||||
return ' '.join([group.name for group in group_list])
|
||||
else:
|
||||
return '%s ...' % ' '.join([group.name for group in group_list[0:2]])
|
||||
|
||||
|
||||
@register.filter(name='str_to_list')
|
||||
def str_to_list(info):
|
||||
"""
|
||||
str to list
|
||||
"""
|
||||
print ast.literal_eval(info), type(ast.literal_eval(info))
|
||||
return ast.literal_eval(info)
|
||||
|
||||
|
||||
@register.filter(name='str_to_dic')
|
||||
def str_to_dic(info):
|
||||
"""
|
||||
str to list
|
||||
"""
|
||||
if '{' in info:
|
||||
info_dic = ast.literal_eval(info).iteritems()
|
||||
else:
|
||||
info_dic = {}
|
||||
return info_dic
|
||||
|
||||
|
||||
@register.filter(name='str_to_code')
|
||||
def str_to_code(char_str):
|
||||
if char_str:
|
||||
return char_str
|
||||
else:
|
||||
return u'空'
|
||||
|
||||
|
||||
@register.filter(name='ip_str_to_list')
|
||||
def ip_str_to_list(ip_str):
|
||||
"""
|
||||
ip str to list
|
||||
"""
|
||||
return ip_str.split(',')
|
||||
|
||||
|
||||
@register.filter(name='key_exist')
|
||||
def key_exist(username):
|
||||
"""
|
||||
ssh key is exist or not
|
||||
"""
|
||||
if os.path.isfile(os.path.join(KEY_DIR, 'user', username+'.pem')):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@register.filter(name='check_role')
|
||||
def check_role(asset_id, user):
|
||||
"""
|
||||
ssh key is exist or not
|
||||
"""
|
||||
return user
|
||||
|
||||
|
||||
@register.filter(name='role_contain_which_sudos')
|
||||
def role_contain_which_sudos(role):
|
||||
"""
|
||||
get role sudo commands
|
||||
"""
|
||||
sudo_names = [sudo.name for sudo in role.sudo.all()]
|
||||
return ','.join(sudo_names)
|
||||
|
||||
|
||||
@register.filter(name='get_push_info')
|
||||
def get_push_info(push_id, arg):
|
||||
push = get_object(PermPush, id=push_id)
|
||||
if push and arg:
|
||||
if arg == 'asset':
|
||||
return [asset.hostname for asset in push.asset.all()]
|
||||
if arg == 'asset_group':
|
||||
return [asset_group.name for asset_group in push.asset_group.all()]
|
||||
if arg == 'role':
|
||||
return [role.name for role in push.role.all()]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
@register.filter(name='get_cpu_core')
|
||||
def get_cpu_core(cpu_info):
|
||||
cpu_core = cpu_info.split('* ')[1] if cpu_info and '*' in cpu_info else cpu_info
|
||||
return cpu_core
|
||||
|
||||
|
||||
@register.filter(name='get_disk_info')
|
||||
def get_disk_info(disk_info):
|
||||
try:
|
||||
disk_size = 0
|
||||
if disk_info:
|
||||
disk_dic = ast.literal_eval(disk_info)
|
||||
for disk, size in disk_dic.items():
|
||||
disk_size += size
|
||||
disk_size = int(disk_size)
|
||||
else:
|
||||
disk_size = ''
|
||||
except Exception:
|
||||
disk_size = disk_info
|
||||
return disk_size
|
||||
|
||||
|
||||
@register.filter(name='user_perm_asset_num')
|
||||
def user_perm_asset_num(user_id):
|
||||
user = get_object(User, id=user_id)
|
||||
if user:
|
||||
user_perm_info = get_group_user_perm(user)
|
||||
return len(user_perm_info.get('asset').keys())
|
||||
else:
|
||||
return 0
|
|
@ -1,20 +0,0 @@
|
|||
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')),
|
||||
)
|
|
@ -1,370 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
from __future__ import division
|
||||
import uuid
|
||||
import urllib
|
||||
|
||||
from django.db.models import Count
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
from django.http import HttpResponseNotFound
|
||||
from django.http import HttpResponse
|
||||
# from jperm.models import Apply
|
||||
import paramiko
|
||||
from jumpserver.api import *
|
||||
from jumpserver.models import Setting
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from jlog.models import Log, FileLog
|
||||
from jperm.perm_api import get_group_user_perm, gen_resource
|
||||
from jasset.models import Asset, IDC
|
||||
from jperm.ansible_api import MyRunner
|
||||
import zipfile
|
||||
|
||||
|
||||
def getDaysByNum(num):
|
||||
"""
|
||||
输出格式:([datetime.date(2015, 11, 6), datetime.date(2015, 11, 8)], ['11-06', '11-08'])
|
||||
"""
|
||||
|
||||
today = datetime.date.today()
|
||||
oneday = datetime.timedelta(days=1)
|
||||
date_li, date_str = [], []
|
||||
for i in range(0, num):
|
||||
today = today-oneday
|
||||
date_li.append(today)
|
||||
date_str.append(str(today)[5:10])
|
||||
date_li.reverse()
|
||||
date_str.reverse()
|
||||
return date_li, date_str
|
||||
|
||||
|
||||
def get_data(x, y, z):
|
||||
pass
|
||||
|
||||
|
||||
def get_data_by_day(date_li, item):
|
||||
data_li = []
|
||||
for d in date_li:
|
||||
logs = Log.objects.filter(start_time__year=d.year,
|
||||
start_time__month=d.month,
|
||||
start_time__day=d.day)
|
||||
if item == 'user':
|
||||
data_li.append(set([log.user for log in logs]))
|
||||
elif item == 'asset':
|
||||
data_li.append(set([log.host for log in logs]))
|
||||
elif item == 'login':
|
||||
data_li.append(logs)
|
||||
else:
|
||||
pass
|
||||
return data_li
|
||||
|
||||
|
||||
def get_count_by_day(date_li, item):
|
||||
data_li = get_data_by_day(date_li, item)
|
||||
data_count_li = []
|
||||
for data in data_li:
|
||||
data_count_li.append(len(data))
|
||||
return data_count_li
|
||||
|
||||
|
||||
def get_count_by_date(date_li, item):
|
||||
data_li = get_data_by_day(date_li, item)
|
||||
data_count_tmp = []
|
||||
for data in data_li:
|
||||
data_count_tmp.extend(list(data))
|
||||
|
||||
return len(set(data_count_tmp))
|
||||
|
||||
|
||||
@require_role(role='user')
|
||||
def index_cu(request):
|
||||
username = request.user.username
|
||||
return HttpResponseRedirect(reverse('user_detail'))
|
||||
|
||||
|
||||
@require_role(role='user')
|
||||
def index(request):
|
||||
li_date, li_str = getDaysByNum(7)
|
||||
today = datetime.datetime.now().day
|
||||
from_week = datetime.datetime.now() - datetime.timedelta(days=7)
|
||||
|
||||
if is_role_request(request, 'user'):
|
||||
return index_cu(request)
|
||||
|
||||
elif is_role_request(request, 'super'):
|
||||
# dashboard 显示汇总
|
||||
users = User.objects.all()
|
||||
hosts = Asset.objects.all()
|
||||
online = Log.objects.filter(is_finished=0)
|
||||
online_host = online.values('host').distinct()
|
||||
online_user = online.values('user').distinct()
|
||||
active_users = User.objects.filter(is_active=1)
|
||||
active_hosts = Asset.objects.filter(is_active=1)
|
||||
|
||||
# 一个月历史汇总
|
||||
date_li, date_str = getDaysByNum(30)
|
||||
date_month = repr(date_str)
|
||||
active_user_per_month = str(get_count_by_day(date_li, 'user'))
|
||||
active_asset_per_month = str(get_count_by_day(date_li, 'asset'))
|
||||
active_login_per_month = str(get_count_by_day(date_li, 'login'))
|
||||
|
||||
# 活跃用户资产图
|
||||
active_user_month = get_count_by_date(date_li, 'user')
|
||||
disabled_user_count = len(users.filter(is_active=False))
|
||||
inactive_user_month = len(users) - active_user_month
|
||||
active_asset_month = get_count_by_date(date_li, 'asset')
|
||||
disabled_asset_count = len(hosts.filter(is_active=False)) if hosts.filter(is_active=False) else 0
|
||||
inactive_asset_month = len(hosts) - active_asset_month if len(hosts) > active_asset_month else 0
|
||||
|
||||
# 一周top10用户和主机
|
||||
week_data = Log.objects.filter(start_time__range=[from_week, datetime.datetime.now()])
|
||||
user_top_ten = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:10]
|
||||
host_top_ten = week_data.values('host').annotate(times=Count('host')).order_by('-times')[:10]
|
||||
|
||||
for user_info in user_top_ten:
|
||||
username = user_info.get('user')
|
||||
last = Log.objects.filter(user=username).latest('start_time')
|
||||
user_info['last'] = last
|
||||
|
||||
for host_info in host_top_ten:
|
||||
host = host_info.get('host')
|
||||
last = Log.objects.filter(host=host).latest('start_time')
|
||||
host_info['last'] = last
|
||||
|
||||
# 一周top5
|
||||
week_users = week_data.values('user').distinct().count()
|
||||
week_hosts = week_data.count()
|
||||
|
||||
user_top_five = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:5]
|
||||
color = ['label-success', 'label-info', 'label-primary', 'label-default', 'label-warnning']
|
||||
|
||||
# 最后10次权限申请
|
||||
# perm apply latest 10
|
||||
# perm_apply_10 = Apply.objects.order_by('-date_add')[:10]
|
||||
|
||||
# 最后10次登陆
|
||||
login_10 = Log.objects.order_by('-start_time')[:10]
|
||||
login_more_10 = Log.objects.order_by('-start_time')[10:21]
|
||||
|
||||
return render_to_response('index.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
def skin_config(request):
|
||||
return render_to_response('skin_config.html')
|
||||
|
||||
|
||||
def is_latest():
|
||||
node = uuid.getnode()
|
||||
jsn = uuid.UUID(int=node).hex[-12:]
|
||||
with open(os.path.join(BASE_DIR, 'version')) as f:
|
||||
current_version = f.read()
|
||||
lastest_version = urllib.urlopen('http://www.jumpserver.org/lastest_version.html?jsn=%s' % jsn).read().strip()
|
||||
|
||||
if current_version != lastest_version:
|
||||
pass
|
||||
|
||||
|
||||
@defend_attack
|
||||
def Login(request):
|
||||
"""登录界面"""
|
||||
error = ''
|
||||
if request.user.is_authenticated():
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
if request.method == 'GET':
|
||||
return render_to_response('login.html')
|
||||
else:
|
||||
username = request.POST.get('username')
|
||||
password = request.POST.get('password')
|
||||
if username and password:
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None:
|
||||
if user.is_active:
|
||||
login(request, user)
|
||||
# c = {}
|
||||
# c.update(csrf(request))
|
||||
# request.session['csrf_token'] = str(c.get('csrf_token'))
|
||||
# user_filter = User.objects.filter(username=username)
|
||||
# if user_filter:
|
||||
# user = user_filter[0]
|
||||
# if PyCrypt.md5_crypt(password) == user.password:
|
||||
# request.session['user_id'] = user.id
|
||||
# user_filter.update(last_login=datetime.datetime.now())
|
||||
if user.role == 'SU':
|
||||
request.session['role_id'] = 2
|
||||
elif user.role == 'GA':
|
||||
request.session['role_id'] = 1
|
||||
else:
|
||||
request.session['role_id'] = 0
|
||||
return HttpResponseRedirect(request.session.get('pre_url', '/'))
|
||||
# response.set_cookie('username', username, expires=604800)
|
||||
# response.set_cookie('seed', PyCrypt.md5_crypt(password), expires=604800)
|
||||
# return response
|
||||
else:
|
||||
error = '用户未激活'
|
||||
else:
|
||||
error = '用户名或密码错误'
|
||||
else:
|
||||
error = '用户名或密码错误'
|
||||
return render_to_response('login.html', {'error': error})
|
||||
|
||||
|
||||
@require_role('user')
|
||||
def Logout(request):
|
||||
logout(request)
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def setting(request):
|
||||
header_title, path1 = '项目设置', '设置'
|
||||
setting_default = get_object(Setting, name='default')
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
setting_raw = request.POST.get('setting', '')
|
||||
if setting_raw == 'default':
|
||||
username = request.POST.get('username', '')
|
||||
port = request.POST.get('port', '')
|
||||
password = request.POST.get('password', '')
|
||||
private_key = request.POST.get('key', '')
|
||||
|
||||
if len(password) > 30:
|
||||
raise ServerError(u'秘密长度不能超过30位!')
|
||||
|
||||
if '' in [username, port]:
|
||||
return ServerError(u'所填内容不能为空, 且密码和私钥填一个')
|
||||
else:
|
||||
private_key_dir = os.path.join(BASE_DIR, 'keys', 'default')
|
||||
private_key_path = os.path.join(private_key_dir, 'admin_user.pem')
|
||||
mkdir(private_key_dir)
|
||||
|
||||
if private_key:
|
||||
with open(private_key_path, 'w') as f:
|
||||
f.write(private_key)
|
||||
os.chmod(private_key_path, 0600)
|
||||
|
||||
if setting_default:
|
||||
if password:
|
||||
password_encode = CRYPTOR.encrypt(password)
|
||||
else:
|
||||
password_encode = password
|
||||
Setting.objects.filter(name='default').update(field1=username, field2=port,
|
||||
field3=password_encode,
|
||||
field4=private_key_path)
|
||||
|
||||
else:
|
||||
password_encode = CRYPTOR.encrypt(password)
|
||||
setting_r = Setting(name='default', field1=username, field2=port,
|
||||
field3=password_encode,
|
||||
field4=private_key_path).save()
|
||||
msg = "设置成功"
|
||||
except ServerError as e:
|
||||
error = e.message
|
||||
return my_render('setting.html', locals(), request)
|
||||
|
||||
|
||||
@login_required(login_url='/login')
|
||||
def upload(request):
|
||||
user = request.user
|
||||
assets = get_group_user_perm(user).get('asset').keys()
|
||||
asset_select = []
|
||||
if request.method == 'POST':
|
||||
remote_ip = request.META.get('REMOTE_ADDR')
|
||||
asset_ids = request.POST.getlist('asset_ids', '')
|
||||
upload_files = request.FILES.getlist('file[]', None)
|
||||
date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
upload_dir = get_tmp_dir()
|
||||
# file_dict = {}
|
||||
for asset_id in asset_ids:
|
||||
asset_select.append(get_object(Asset, id=asset_id))
|
||||
|
||||
if not set(asset_select).issubset(set(assets)):
|
||||
illegal_asset = set(asset_select).issubset(set(assets))
|
||||
return HttpResponse('没有权限的服务器 %s' % ','.join([asset.hostname for asset in illegal_asset]))
|
||||
|
||||
for upload_file in upload_files:
|
||||
file_path = '%s/%s' % (upload_dir, upload_file.name)
|
||||
with open(file_path, 'w') as f:
|
||||
for chunk in upload_file.chunks():
|
||||
f.write(chunk)
|
||||
|
||||
res = gen_resource({'user': user, 'asset': asset_select})
|
||||
runner = MyRunner(res)
|
||||
runner.run('copy', module_args='src=%s dest=%s directory_mode'
|
||||
% (upload_dir, '/tmp'), pattern='*')
|
||||
ret = runner.results
|
||||
logger.debug(ret)
|
||||
FileLog(user=request.user.username, host=' '.join([asset.hostname for asset in asset_select]),
|
||||
filename=' '.join([f.name for f in upload_files]), type='upload', remote_ip=remote_ip,
|
||||
result=ret).save()
|
||||
if ret.get('failed'):
|
||||
error = u'上传目录: %s <br> 上传失败: [ %s ] <br>上传成功 [ %s ]' % (upload_dir,
|
||||
', '.join(ret.get('failed').keys()),
|
||||
', '.join(ret.get('ok').keys()))
|
||||
return HttpResponse(error, status=500)
|
||||
msg = u'上传目录: %s <br> 传送成功 [ %s ]' % (upload_dir, ', '.join(ret.get('ok').keys()))
|
||||
return HttpResponse(msg)
|
||||
return my_render('upload.html', locals(), request)
|
||||
|
||||
|
||||
@login_required(login_url='/login')
|
||||
def download(request):
|
||||
user = request.user
|
||||
assets = get_group_user_perm(user).get('asset').keys()
|
||||
asset_select = []
|
||||
if request.method == 'POST':
|
||||
remote_ip = request.META.get('REMOTE_ADDR')
|
||||
asset_ids = request.POST.getlist('asset_ids', '')
|
||||
file_path = request.POST.get('file_path')
|
||||
date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
upload_dir = get_tmp_dir()
|
||||
for asset_id in asset_ids:
|
||||
asset_select.append(get_object(Asset, id=asset_id))
|
||||
|
||||
if not set(asset_select).issubset(set(assets)):
|
||||
illegal_asset = set(asset_select).issubset(set(assets))
|
||||
return HttpResponse(u'没有权限的服务器 %s' % ','.join([asset.hostname for asset in illegal_asset]))
|
||||
|
||||
res = gen_resource({'user': user, 'asset': asset_select})
|
||||
runner = MyRunner(res)
|
||||
runner.run('fetch', module_args='src=%s dest=%s' % (file_path, upload_dir), pattern='*')
|
||||
FileLog(user=request.user.username, host=' '.join([asset.hostname for asset in asset_select]),
|
||||
filename=file_path, type='download', remote_ip=remote_ip, result=runner.results).save()
|
||||
logger.debug(runner.results)
|
||||
tmp_dir_name = os.path.basename(upload_dir)
|
||||
file_zip = '/tmp/'+tmp_dir_name+'.zip'
|
||||
zf = zipfile.ZipFile(file_zip, "w", zipfile.ZIP_DEFLATED)
|
||||
for dirname, subdirs, files in os.walk(upload_dir):
|
||||
zf.write(dirname)
|
||||
for filename in files:
|
||||
zf.write(os.path.join(dirname, filename))
|
||||
zf.close()
|
||||
f = open(file_zip)
|
||||
data = f.read()
|
||||
f.close()
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s.zip' % tmp_dir_name
|
||||
return response
|
||||
|
||||
return render_to_response('download.html', locals(), context_instance=RequestContext(request))
|
||||
|
||||
|
||||
@login_required(login_url='/login')
|
||||
def exec_cmd(request):
|
||||
role = request.GET.get('role')
|
||||
check_assets = request.GET.get('check_assets', '')
|
||||
web_terminal_uri = '/ws/exec?role=%s' % (role)
|
||||
return my_render('exec_cmd.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('user')
|
||||
def web_terminal(request):
|
||||
asset_id = request.GET.get('id')
|
||||
role_name = request.GET.get('role')
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
if asset:
|
||||
hostname = asset.hostname
|
||||
return render_to_response('jlog/web_terminal.html', locals())
|
||||
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""
|
||||
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()
|
|
@ -1,3 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
|
@ -1,54 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
import time
|
||||
# from jasset.models import Asset, AssetGroup
|
||||
|
||||
|
||||
class UserGroup(models.Model):
|
||||
name = models.CharField(max_length=80, unique=True)
|
||||
comment = models.CharField(max_length=160, blank=True, null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
USER_ROLE_CHOICES = (
|
||||
('SU', 'SuperUser'),
|
||||
('GA', 'GroupAdmin'),
|
||||
('CU', 'CommonUser'),
|
||||
)
|
||||
name = models.CharField(max_length=80)
|
||||
uuid = models.CharField(max_length=100)
|
||||
role = models.CharField(max_length=2, choices=USER_ROLE_CHOICES, default='CU')
|
||||
group = models.ManyToManyField(UserGroup)
|
||||
ssh_key_pwd = models.CharField(max_length=200)
|
||||
# is_active = models.BooleanField(default=True)
|
||||
# last_login = models.DateTimeField(null=True)
|
||||
# date_joined = models.DateTimeField(null=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.username
|
||||
|
||||
|
||||
class AdminGroup(models.Model):
|
||||
"""
|
||||
under the user control group
|
||||
用户可以管理的用户组,或组的管理员是该用户
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(User)
|
||||
group = models.ForeignKey(UserGroup)
|
||||
|
||||
def __unicode__(self):
|
||||
return '%s: %s' % (self.user.username, self.group.name)
|
||||
|
||||
|
||||
class Document(models.Model):
|
||||
def upload_to(self, filename):
|
||||
return 'upload/'+str(self.user.id)+time.strftime('/%Y/%m/%d/', time.localtime())+filename
|
||||
|
||||
docfile = models.FileField(upload_to=upload_to)
|
||||
user = models.ForeignKey(User)
|
|
@ -1,25 +0,0 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from jumpserver.api import view_splitter
|
||||
from juser.views import *
|
||||
|
||||
urlpatterns = patterns('juser.views',
|
||||
# Examples:
|
||||
# url(r'^$', 'jumpserver.views.home', name='home'),
|
||||
# url(r'^blog/', include('blog.urls')),
|
||||
url(r'^group/add/$', 'group_add', name='user_group_add'),
|
||||
url(r'^group/list/$', 'group_list', name='user_group_list'),
|
||||
url(r'^group/del/$', 'group_del', name='user_group_del'),
|
||||
url(r'^group/edit/$', 'group_edit', name='user_group_edit'),
|
||||
url(r'^user/add/$', 'user_add', name='user_add'),
|
||||
url(r'^user/del/$', 'user_del', name='user_del'),
|
||||
url(r'^user/list/$', 'user_list', name='user_list'),
|
||||
url(r'^user/edit/$', 'user_edit', name='user_edit'),
|
||||
url(r'^user/detail/$', 'user_detail', name='user_detail'),
|
||||
url(r'^user/profile/$', 'profile', name='user_profile'),
|
||||
url(r'^user/update/$', 'change_info', name='user_update'),
|
||||
url(r'^mail/retry/$', 'send_mail_retry', name='mail_retry'),
|
||||
url(r'^password/reset/$', 'reset_password', name='password_reset'),
|
||||
url(r'^password/forget/$', 'forget_password', name='password_forget'),
|
||||
url(r'^key/gen/$', 'regen_ssh_key', name='key_gen'),
|
||||
url(r'^key/down/$', 'down_key', name='key_down'),
|
||||
)
|
|
@ -1,202 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
from Crypto.PublicKey import RSA
|
||||
from subprocess import call
|
||||
|
||||
from juser.models import AdminGroup
|
||||
from jumpserver.api import *
|
||||
from jumpserver.settings import BASE_DIR, EMAIL_HOST_USER as MAIL_FROM
|
||||
|
||||
|
||||
def group_add_user(group, user_id=None, username=None):
|
||||
"""
|
||||
用户组中添加用户
|
||||
UserGroup Add a user
|
||||
"""
|
||||
if user_id:
|
||||
user = get_object(User, id=user_id)
|
||||
else:
|
||||
user = get_object(User, username=username)
|
||||
|
||||
if user:
|
||||
group.user_set.add(user)
|
||||
|
||||
|
||||
def db_add_group(**kwargs):
|
||||
"""
|
||||
add a user group in database
|
||||
数据库中添加用户组
|
||||
"""
|
||||
name = kwargs.get('name')
|
||||
group = get_object(UserGroup, name=name)
|
||||
users = kwargs.pop('users_id')
|
||||
|
||||
if not group:
|
||||
group = UserGroup(**kwargs)
|
||||
group.save()
|
||||
for user_id in users:
|
||||
group_add_user(group, user_id)
|
||||
|
||||
|
||||
def group_update_member(group_id, users_id_list):
|
||||
"""
|
||||
user group update member
|
||||
用户组更新成员
|
||||
"""
|
||||
group = get_object(UserGroup, id=group_id)
|
||||
if group:
|
||||
group.user_set.clear()
|
||||
for user_id in users_id_list:
|
||||
user = get_object(UserGroup, id=user_id)
|
||||
if isinstance(user, UserGroup):
|
||||
group.user_set.add(user)
|
||||
|
||||
|
||||
def db_add_user(**kwargs):
|
||||
"""
|
||||
add a user in database
|
||||
数据库中添加用户
|
||||
"""
|
||||
groups_post = kwargs.pop('groups')
|
||||
admin_groups = kwargs.pop('admin_groups')
|
||||
role = kwargs.get('role', 'CU')
|
||||
user = User(**kwargs)
|
||||
user.set_password(kwargs.get('password'))
|
||||
user.save()
|
||||
if groups_post:
|
||||
group_select = []
|
||||
for group_id in groups_post:
|
||||
group = UserGroup.objects.filter(id=group_id)
|
||||
group_select.extend(group)
|
||||
user.group = group_select
|
||||
|
||||
if admin_groups and role == 'GA': # 如果是组管理员就要添加组管理员和组到管理组中
|
||||
for group_id in admin_groups:
|
||||
group = get_object(UserGroup, id=group_id)
|
||||
if group:
|
||||
AdminGroup(user=user, group=group).save()
|
||||
return user
|
||||
|
||||
|
||||
def db_update_user(**kwargs):
|
||||
"""
|
||||
update a user info in database
|
||||
数据库更新用户信息
|
||||
"""
|
||||
groups_post = kwargs.pop('groups')
|
||||
admin_groups_post = kwargs.pop('admin_groups')
|
||||
user_id = kwargs.pop('user_id')
|
||||
user = User.objects.filter(id=user_id)
|
||||
if user:
|
||||
user_get = user[0]
|
||||
password = kwargs.pop('password')
|
||||
user.update(**kwargs)
|
||||
if password.strip():
|
||||
user_get.set_password(password)
|
||||
user_get.save()
|
||||
else:
|
||||
return None
|
||||
|
||||
group_select = []
|
||||
if groups_post:
|
||||
for group_id in groups_post:
|
||||
group = UserGroup.objects.filter(id=group_id)
|
||||
group_select.extend(group)
|
||||
user_get.group = group_select
|
||||
|
||||
if admin_groups_post != '':
|
||||
user_get.admingroup_set.all().delete()
|
||||
for group_id in admin_groups_post:
|
||||
group = get_object(UserGroup, id=group_id)
|
||||
AdminGroup(user=user, group=group).save()
|
||||
|
||||
|
||||
def db_del_user(username):
|
||||
"""
|
||||
delete a user from database
|
||||
从数据库中删除用户
|
||||
"""
|
||||
user = get_object(User, username=username)
|
||||
if user:
|
||||
user.delete()
|
||||
|
||||
|
||||
def gen_ssh_key(username, password='',
|
||||
key_dir=os.path.join(KEY_DIR, 'user'),
|
||||
authorized_keys=True, home="/home", length=2048):
|
||||
"""
|
||||
generate a user ssh key in a property dir
|
||||
生成一个用户ssh密钥对
|
||||
"""
|
||||
logger.debug('生成ssh key, 并设置authorized_keys')
|
||||
private_key_file = os.path.join(key_dir, username+'.pem')
|
||||
mkdir(key_dir, mode=777)
|
||||
if os.path.isfile(private_key_file):
|
||||
os.unlink(private_key_file)
|
||||
ret = bash('echo -e "y\n"|ssh-keygen -t rsa -f %s -b %s -P "%s"' % (private_key_file, length, password))
|
||||
|
||||
if authorized_keys:
|
||||
auth_key_dir = os.path.join(home, username, '.ssh')
|
||||
mkdir(auth_key_dir, username=username, mode=700)
|
||||
authorized_key_file = os.path.join(auth_key_dir, 'authorized_keys')
|
||||
with open(private_key_file+'.pub') as pub_f:
|
||||
with open(authorized_key_file, 'w') as auth_f:
|
||||
auth_f.write(pub_f.read())
|
||||
os.chmod(authorized_key_file, 0600)
|
||||
chown(authorized_key_file, username)
|
||||
|
||||
|
||||
def server_add_user(username, ssh_key_pwd=''):
|
||||
"""
|
||||
add a system user in jumpserver
|
||||
在jumpserver服务器上添加一个用户
|
||||
"""
|
||||
bash("adduser -s '%s' '%s'" % (os.path.join(BASE_DIR, 'init.sh'), username))
|
||||
gen_ssh_key(username, ssh_key_pwd)
|
||||
|
||||
|
||||
def user_add_mail(user, kwargs):
|
||||
"""
|
||||
add user send mail
|
||||
发送用户添加邮件
|
||||
"""
|
||||
user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'}
|
||||
mail_title = u'恭喜你的跳板机用户 %s 添加成功 Jumpserver' % user.name
|
||||
mail_msg = u"""
|
||||
Hi, %s
|
||||
您的用户名: %s
|
||||
您的权限: %s
|
||||
您的web登录密码: %s
|
||||
您的ssh密钥文件密码: %s
|
||||
密钥下载地址: %s/juser/key/down/?uuid=%s
|
||||
说明: 请登陆跳板机后台下载密钥, 然后使用密钥登陆跳板机!
|
||||
""" % (user.name, user.username, user_role.get(user.role, u'普通用户'),
|
||||
kwargs.get('password'), kwargs.get('ssh_key_pwd'), URL, user.uuid)
|
||||
send_mail(mail_title, mail_msg, MAIL_FROM, [user.email], fail_silently=False)
|
||||
|
||||
|
||||
def server_del_user(username):
|
||||
"""
|
||||
delete a user from jumpserver linux system
|
||||
删除系统上的某用户
|
||||
"""
|
||||
bash('userdel -r -f %s' % username)
|
||||
logger.debug('rm -f %s/%s_*.pem' % (os.path.join(KEY_DIR, 'user'), username))
|
||||
bash('rm -f %s/%s_*.pem' % (os.path.join(KEY_DIR, 'user'), username))
|
||||
bash('rm -f %s/%s.pem*' % (os.path.join(KEY_DIR, 'user'), username))
|
||||
|
||||
|
||||
def get_display_msg(user, password='', ssh_key_pwd='', send_mail_need=False):
|
||||
if send_mail_need:
|
||||
msg = u'添加用户 %s 成功! 用户密码已发送到 %s 邮箱!' % (user.name, user.email)
|
||||
else:
|
||||
msg = u"""
|
||||
跳板机地址: %s <br />
|
||||
用户名:%s <br />
|
||||
密码:%s <br />
|
||||
密钥密码:%s <br />
|
||||
密钥下载url: %s/juser/key/down/?uuid=%s <br />
|
||||
该账号密码可以登陆web和跳板机。
|
||||
""" % (URL, user.username, password, ssh_key_pwd, URL, user.uuid)
|
||||
return msg
|
||||
|
468
juser/views.py
|
@ -1,468 +0,0 @@
|
|||
# coding: utf-8
|
||||
# Author: Guanghongwei
|
||||
# Email: ibuler@qq.com
|
||||
|
||||
# import random
|
||||
# from Crypto.PublicKey import RSA
|
||||
import uuid
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.db.models import Q
|
||||
from juser.user_api import *
|
||||
from jperm.perm_api import get_group_user_perm
|
||||
|
||||
MAIL_FROM = EMAIL_HOST_USER
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def group_add(request):
|
||||
"""
|
||||
group add view for route
|
||||
添加用户组的视图
|
||||
"""
|
||||
error = ''
|
||||
msg = ''
|
||||
header_title, path1, path2 = '添加用户组', '用户管理', '添加用户组'
|
||||
user_all = User.objects.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
group_name = request.POST.get('group_name', '')
|
||||
users_selected = request.POST.getlist('users_selected', '')
|
||||
comment = request.POST.get('comment', '')
|
||||
|
||||
try:
|
||||
if not group_name:
|
||||
error = u'组名 不能为空'
|
||||
raise ServerError(error)
|
||||
|
||||
if UserGroup.objects.filter(name=group_name):
|
||||
error = u'组名已存在'
|
||||
raise ServerError(error)
|
||||
db_add_group(name=group_name, users_id=users_selected, comment=comment)
|
||||
except ServerError:
|
||||
pass
|
||||
except TypeError:
|
||||
error = u'添加小组失败'
|
||||
else:
|
||||
msg = u'添加组 %s 成功' % group_name
|
||||
|
||||
return my_render('juser/group_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def group_list(request):
|
||||
"""
|
||||
list user group
|
||||
用户组列表
|
||||
"""
|
||||
header_title, path1, path2 = '查看用户组', '用户管理', '查看用户组'
|
||||
keyword = request.GET.get('search', '')
|
||||
user_group_list = UserGroup.objects.all().order_by('name')
|
||||
group_id = request.GET.get('id', '')
|
||||
|
||||
if keyword:
|
||||
user_group_list = user_group_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword))
|
||||
|
||||
if group_id:
|
||||
user_group_list = user_group_list.filter(id=int(group_id))
|
||||
|
||||
user_group_list, p, user_groups, page_range, current_page, show_first, show_end = pages(user_group_list, request)
|
||||
return my_render('juser/group_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def group_del(request):
|
||||
"""
|
||||
del a group
|
||||
删除用户组
|
||||
"""
|
||||
group_ids = request.GET.get('id', '')
|
||||
group_id_list = group_ids.split(',')
|
||||
for group_id in group_id_list:
|
||||
UserGroup.objects.filter(id=group_id).delete()
|
||||
|
||||
return HttpResponse('删除成功')
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def group_edit(request):
|
||||
error = ''
|
||||
msg = ''
|
||||
header_title, path1, path2 = '编辑用户组', '用户管理', '编辑用户组'
|
||||
|
||||
if request.method == 'GET':
|
||||
group_id = request.GET.get('id', '')
|
||||
user_group = get_object(UserGroup, id=group_id)
|
||||
# user_group = UserGroup.objects.get(id=group_id)
|
||||
users_selected = User.objects.filter(group=user_group)
|
||||
users_remain = User.objects.filter(~Q(group=user_group))
|
||||
users_all = User.objects.all()
|
||||
|
||||
elif request.method == 'POST':
|
||||
group_id = request.POST.get('group_id', '')
|
||||
group_name = request.POST.get('group_name', '')
|
||||
comment = request.POST.get('comment', '')
|
||||
users_selected = request.POST.getlist('users_selected')
|
||||
|
||||
try:
|
||||
if '' in [group_id, group_name]:
|
||||
raise ServerError('组名不能为空')
|
||||
|
||||
if len(UserGroup.objects.filter(name=group_name)) > 1:
|
||||
raise ServerError(u'%s 用户组已存在' % group_name)
|
||||
# add user group
|
||||
user_group = get_object_or_404(UserGroup, id=group_id)
|
||||
user_group.user_set.clear()
|
||||
|
||||
for user in User.objects.filter(id__in=users_selected):
|
||||
user.group.add(UserGroup.objects.get(id=group_id))
|
||||
|
||||
user_group.name = group_name
|
||||
user_group.comment = comment
|
||||
user_group.save()
|
||||
except ServerError, e:
|
||||
error = e
|
||||
|
||||
if not error:
|
||||
return HttpResponseRedirect(reverse('user_group_list'))
|
||||
else:
|
||||
users_all = User.objects.all()
|
||||
users_selected = User.objects.filter(group=user_group)
|
||||
users_remain = User.objects.filter(~Q(group=user_group))
|
||||
|
||||
return my_render('juser/group_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def user_add(request):
|
||||
error = ''
|
||||
msg = ''
|
||||
header_title, path1, path2 = '添加用户', '用户管理', '添加用户'
|
||||
user_role = {'SU': u'超级管理员', 'CU': u'普通用户'}
|
||||
group_all = UserGroup.objects.all()
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.POST.get('username', '')
|
||||
password = PyCrypt.gen_rand_pass(16)
|
||||
name = request.POST.get('name', '')
|
||||
email = request.POST.get('email', '')
|
||||
groups = request.POST.getlist('groups', [])
|
||||
admin_groups = request.POST.getlist('admin_groups', [])
|
||||
role = request.POST.get('role', 'CU')
|
||||
uuid_r = uuid.uuid4().get_hex()
|
||||
ssh_key_pwd = PyCrypt.gen_rand_pass(16)
|
||||
extra = request.POST.getlist('extra', [])
|
||||
is_active = False if '0' in extra else True
|
||||
send_mail_need = True if '1' in extra else False
|
||||
|
||||
try:
|
||||
if '' in [username, password, ssh_key_pwd, name, role]:
|
||||
error = u'带*内容不能为空'
|
||||
raise ServerError
|
||||
check_user_is_exist = User.objects.filter(username=username)
|
||||
if check_user_is_exist:
|
||||
error = u'用户 %s 已存在' % username
|
||||
raise ServerError
|
||||
|
||||
except ServerError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
user = db_add_user(username=username, name=name,
|
||||
password=password,
|
||||
email=email, role=role, uuid=uuid_r,
|
||||
groups=groups, admin_groups=admin_groups,
|
||||
ssh_key_pwd=ssh_key_pwd,
|
||||
is_active=is_active,
|
||||
date_joined=datetime.datetime.now())
|
||||
server_add_user(username=username, ssh_key_pwd=ssh_key_pwd)
|
||||
user = get_object(User, username=username)
|
||||
if groups:
|
||||
user_groups = []
|
||||
for user_group_id in groups:
|
||||
user_groups.extend(UserGroup.objects.filter(id=user_group_id))
|
||||
|
||||
except IndexError, e:
|
||||
error = u'添加用户 %s 失败 %s ' % (username, e)
|
||||
try:
|
||||
db_del_user(username)
|
||||
server_del_user(username)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
if MAIL_ENABLE and send_mail_need:
|
||||
user_add_mail(user, kwargs=locals())
|
||||
msg = get_display_msg(user, password=password, ssh_key_pwd=ssh_key_pwd, send_mail_need=send_mail_need)
|
||||
return my_render('juser/user_add.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def user_list(request):
|
||||
user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'}
|
||||
header_title, path1, path2 = '查看用户', '用户管理', '用户列表'
|
||||
keyword = request.GET.get('keyword', '')
|
||||
gid = request.GET.get('gid', '')
|
||||
users_list = User.objects.all().order_by('username')
|
||||
|
||||
if gid:
|
||||
user_group = UserGroup.objects.filter(id=gid)
|
||||
if user_group:
|
||||
user_group = user_group[0]
|
||||
users_list = user_group.user_set.all()
|
||||
|
||||
if keyword:
|
||||
users_list = users_list.filter(Q(username__icontains=keyword) | Q(name__icontains=keyword)).order_by('username')
|
||||
|
||||
users_list, p, users, page_range, current_page, show_first, show_end = pages(users_list, request)
|
||||
|
||||
return my_render('juser/user_list.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='user')
|
||||
def user_detail(request):
|
||||
header_title, path1, path2 = '用户详情', '用户管理', '用户详情'
|
||||
if request.session.get('role_id') == 0:
|
||||
user_id = request.user.id
|
||||
else:
|
||||
user_id = request.GET.get('id', '')
|
||||
|
||||
user = get_object(User, id=user_id)
|
||||
if not user:
|
||||
return HttpResponseRedirect(reverse('user_list'))
|
||||
|
||||
user_perm_info = get_group_user_perm(user)
|
||||
role_assets = user_perm_info.get('role')
|
||||
user_log_ten = Log.objects.filter(user=user.username).order_by('id')[0:10]
|
||||
user_log_last = Log.objects.filter(user=user.username).order_by('id')[0:50]
|
||||
user_log_last_num = len(user_log_last)
|
||||
|
||||
return my_render('juser/user_detail.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='admin')
|
||||
def user_del(request):
|
||||
if request.method == "GET":
|
||||
user_ids = request.GET.get('id', '')
|
||||
user_id_list = user_ids.split(',')
|
||||
elif request.method == "POST":
|
||||
user_ids = request.POST.get('id', '')
|
||||
user_id_list = user_ids.split(',')
|
||||
else:
|
||||
return HttpResponse('错误请求')
|
||||
|
||||
for user_id in user_id_list:
|
||||
user = get_object(User, id=user_id)
|
||||
if user and user.username != 'admin':
|
||||
logger.debug(u"删除用户 %s " % user.username)
|
||||
server_del_user(user.username)
|
||||
user.delete()
|
||||
return HttpResponse('删除成功')
|
||||
|
||||
|
||||
@require_role('admin')
|
||||
def send_mail_retry(request):
|
||||
uuid_r = request.GET.get('uuid', '1')
|
||||
user = get_object(User, uuid=uuid_r)
|
||||
msg = u"""
|
||||
跳板机地址: %s
|
||||
用户名:%s
|
||||
重设密码:%s/juser/password/forget/
|
||||
请登录web点击个人信息页面重新生成ssh密钥
|
||||
""" % (URL, user.username, URL)
|
||||
|
||||
try:
|
||||
send_mail(u'邮件重发', msg, MAIL_FROM, [user.email], fail_silently=False)
|
||||
except IndexError:
|
||||
return Http404
|
||||
return HttpResponse('发送成功')
|
||||
|
||||
|
||||
@defend_attack
|
||||
def forget_password(request):
|
||||
if request.method == 'POST':
|
||||
defend_attack(request)
|
||||
email = request.POST.get('email', '')
|
||||
username = request.POST.get('username', '')
|
||||
name = request.POST.get('name', '')
|
||||
user = get_object(User, username=username, email=email, name=name)
|
||||
if user:
|
||||
timestamp = int(time.time())
|
||||
hash_encode = PyCrypt.md5_crypt(str(user.uuid) + str(timestamp) + KEY)
|
||||
msg = u"""
|
||||
Hi %s, 请点击下面链接重设密码!
|
||||
%s/juser/password/reset/?uuid=%s×tamp=%s&hash=%s
|
||||
""" % (user.name, URL, user.uuid, timestamp, hash_encode)
|
||||
send_mail('忘记跳板机密码', msg, MAIL_FROM, [email], fail_silently=False)
|
||||
msg = u'请登陆邮箱,点击邮件重设密码'
|
||||
return http_success(request, msg)
|
||||
else:
|
||||
error = u'用户不存在或邮件地址错误'
|
||||
|
||||
return render_to_response('juser/forget_password.html', locals())
|
||||
|
||||
|
||||
@defend_attack
|
||||
def reset_password(request):
|
||||
uuid_r = request.GET.get('uuid', '')
|
||||
timestamp = request.GET.get('timestamp', '')
|
||||
hash_encode = request.GET.get('hash', '')
|
||||
action = '/juser/password/reset/?uuid=%s×tamp=%s&hash=%s' % (uuid_r, timestamp, hash_encode)
|
||||
|
||||
if hash_encode == PyCrypt.md5_crypt(uuid_r + timestamp + KEY):
|
||||
if int(time.time()) - int(timestamp) > 600:
|
||||
return http_error(request, u'链接已超时')
|
||||
else:
|
||||
return HttpResponse('hash校验失败')
|
||||
|
||||
if request.method == 'POST':
|
||||
password = request.POST.get('password')
|
||||
password_confirm = request.POST.get('password_confirm')
|
||||
print password, password_confirm
|
||||
if password != password_confirm:
|
||||
return HttpResponse('密码不匹配')
|
||||
else:
|
||||
user = get_object(User, uuid=uuid_r)
|
||||
if user:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
return http_success(request, u'密码重设成功')
|
||||
else:
|
||||
return HttpResponse('用户不存在')
|
||||
|
||||
else:
|
||||
return render_to_response('juser/reset_password.html', locals())
|
||||
|
||||
return http_error(request, u'错误请求')
|
||||
|
||||
|
||||
@require_role(role='super')
|
||||
def user_edit(request):
|
||||
header_title, path1, path2 = '编辑用户', '用户管理', '编辑用户'
|
||||
if request.method == 'GET':
|
||||
user_id = request.GET.get('id', '')
|
||||
if not user_id:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
user_role = {'SU': u'超级管理员', 'CU': u'普通用户'}
|
||||
user = get_object(User, id=user_id)
|
||||
group_all = UserGroup.objects.all()
|
||||
if user:
|
||||
groups_str = ' '.join([str(group.id) for group in user.group.all()])
|
||||
admin_groups_str = ' '.join([str(admin_group.group.id) for admin_group in user.admingroup_set.all()])
|
||||
|
||||
else:
|
||||
user_id = request.GET.get('id', '')
|
||||
password = request.POST.get('password', '')
|
||||
name = request.POST.get('name', '')
|
||||
email = request.POST.get('email', '')
|
||||
groups = request.POST.getlist('groups', [])
|
||||
role_post = request.POST.get('role', 'CU')
|
||||
admin_groups = request.POST.getlist('admin_groups', [])
|
||||
extra = request.POST.getlist('extra', [])
|
||||
is_active = True if '0' in extra else False
|
||||
email_need = True if '1' in extra else False
|
||||
user_role = {'SU': u'超级管理员', 'GA': u'部门管理员', 'CU': u'普通用户'}
|
||||
|
||||
if user_id:
|
||||
user = get_object(User, id=user_id)
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('user_list'))
|
||||
|
||||
db_update_user(user_id=user_id,
|
||||
password=password,
|
||||
name=name,
|
||||
email=email,
|
||||
groups=groups,
|
||||
admin_groups=admin_groups,
|
||||
role=role_post,
|
||||
is_active=is_active)
|
||||
|
||||
if email_need:
|
||||
msg = u"""
|
||||
Hi %s:
|
||||
您的信息已修改,请登录跳板机查看详细信息
|
||||
地址:%s
|
||||
用户名: %s
|
||||
密码:%s (如果密码为None代表密码为原密码)
|
||||
权限::%s
|
||||
|
||||
""" % (user.name, URL, user.username, password, user_role.get(role_post, u''))
|
||||
send_mail('您的信息已修改', msg, MAIL_FROM, [email], fail_silently=False)
|
||||
|
||||
return HttpResponseRedirect(reverse('user_list'))
|
||||
return my_render('juser/user_edit.html', locals(), request)
|
||||
|
||||
|
||||
@require_role('user')
|
||||
def profile(request):
|
||||
user_id = request.user.id
|
||||
if not user_id:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
user = User.objects.get(id=user_id)
|
||||
return my_render('juser/profile.html', locals(), request)
|
||||
|
||||
|
||||
def change_info(request):
|
||||
header_title, path1, path2 = '修改信息', '用户管理', '修改个人信息'
|
||||
user_id = request.user.id
|
||||
user = User.objects.get(id=user_id)
|
||||
error = ''
|
||||
if not user:
|
||||
return HttpResponseRedirect(reverse('index'))
|
||||
|
||||
if request.method == 'POST':
|
||||
name = request.POST.get('name', '')
|
||||
password = request.POST.get('password', '')
|
||||
email = request.POST.get('email', '')
|
||||
|
||||
if '' in [name, email]:
|
||||
error = '不能为空'
|
||||
|
||||
if not error:
|
||||
user.name = name
|
||||
user.email = email
|
||||
user.save()
|
||||
if len(password) > 0:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
msg = '修改成功'
|
||||
|
||||
return my_render('juser/change_info.html', locals(), request)
|
||||
|
||||
|
||||
@require_role(role='user')
|
||||
def regen_ssh_key(request):
|
||||
uuid_r = request.GET.get('uuid', '')
|
||||
user = get_object(User, uuid=uuid_r)
|
||||
if not user:
|
||||
return HttpResponse('没有该用户')
|
||||
|
||||
username = user.username
|
||||
ssh_key_pass = PyCrypt.gen_rand_pass(16)
|
||||
gen_ssh_key(username, ssh_key_pass)
|
||||
return HttpResponse('ssh密钥已生成,密码为 %s, 请到下载页面下载' % ssh_key_pass)
|
||||
|
||||
|
||||
@require_role(role='user')
|
||||
def down_key(request):
|
||||
if is_role_request(request, 'super'):
|
||||
uuid_r = request.GET.get('uuid', '')
|
||||
else:
|
||||
uuid_r = request.user.uuid
|
||||
if uuid_r:
|
||||
user = get_object(User, uuid=uuid_r)
|
||||
if user:
|
||||
username = user.username
|
||||
private_key_file = os.path.join(KEY_DIR, 'user', username+'.pem')
|
||||
print private_key_file
|
||||
if os.path.isfile(private_key_file):
|
||||
f = open(private_key_file)
|
||||
data = f.read()
|
||||
f.close()
|
||||
response = HttpResponse(data, content_type='application/octet-stream')
|
||||
response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(private_key_file)
|
||||
if request.user.role == 'CU':
|
||||
os.unlink(private_key_file)
|
||||
return response
|
||||
return HttpResponse('No Key File. Contact Admin.')
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
看山是山,看水是水
|
||||
看山不是山,看水不是水
|
||||
看山是山,看水是水
|
|
@ -1 +0,0 @@
|
|||
永远年轻,永远热泪盈眶
|
10
manage.py
|
@ -1,10 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "jumpserver.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
execute_from_command_line(sys.argv)
|
519
run_server.py
|
@ -1,519 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# coding: utf-8
|
||||
|
||||
import time
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import os.path
|
||||
import threading
|
||||
import re
|
||||
import functools
|
||||
from django.core.signals import request_started, request_finished
|
||||
|
||||
import tornado.ioloop
|
||||
import tornado.options
|
||||
import tornado.web
|
||||
import tornado.websocket
|
||||
import tornado.httpserver
|
||||
import tornado.gen
|
||||
import tornado.httpclient
|
||||
from tornado.websocket import WebSocketClosedError
|
||||
|
||||
from tornado.options import define, options
|
||||
from pyinotify import WatchManager, ProcessEvent, IN_DELETE, IN_CREATE, IN_MODIFY, AsyncNotifier
|
||||
import select
|
||||
|
||||
from connect import Tty, User, Asset, PermRole, logger, get_object, gen_resource
|
||||
from connect import TtyLog, Log, Session, user_have_perm, get_group_user_perm, MyRunner, ExecLog
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings'
|
||||
from jumpserver.settings import IP, PORT
|
||||
|
||||
define("port", default=PORT, help="run on the given port", type=int)
|
||||
define("host", default=IP, help="run port on given host", type=str)
|
||||
from jlog.views import TermLogRecorder
|
||||
|
||||
|
||||
def django_request_support(func):
|
||||
@functools.wraps(func)
|
||||
def _deco(*args, **kwargs):
|
||||
request_started.send_robust(func)
|
||||
response = func(*args, **kwargs)
|
||||
request_finished.send_robust(func)
|
||||
return response
|
||||
|
||||
return _deco
|
||||
|
||||
|
||||
def require_auth(role='user'):
|
||||
def _deco(func):
|
||||
def _deco2(request, *args, **kwargs):
|
||||
if request.get_cookie('sessionid'):
|
||||
session_key = request.get_cookie('sessionid')
|
||||
else:
|
||||
session_key = request.get_argument('sessionid', '')
|
||||
|
||||
logger.debug('Websocket: session_key: %s' % session_key)
|
||||
if session_key:
|
||||
session = get_object(Session, session_key=session_key)
|
||||
logger.debug('Websocket: session: %s' % session)
|
||||
if session and datetime.datetime.now() < session.expire_date:
|
||||
user_id = session.get_decoded().get('_auth_user_id')
|
||||
request.user_id = user_id
|
||||
user = get_object(User, id=user_id)
|
||||
if user:
|
||||
logger.debug('Websocket: user [ %s ] request websocket' % user.username)
|
||||
request.user = user
|
||||
if role == 'admin':
|
||||
if user.role in ['SU', 'GA']:
|
||||
return func(request, *args, **kwargs)
|
||||
logger.debug('Websocket: user [ %s ] is not admin.' % user.username)
|
||||
else:
|
||||
return func(request, *args, **kwargs)
|
||||
else:
|
||||
logger.debug('Websocket: session expired: %s' % session_key)
|
||||
try:
|
||||
request.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
logger.warning('Websocket: Request auth failed.')
|
||||
|
||||
return _deco2
|
||||
|
||||
return _deco
|
||||
|
||||
|
||||
class MyThread(threading.Thread):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MyThread, self).__init__(*args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
super(MyThread, self).run()
|
||||
except WebSocketClosedError:
|
||||
pass
|
||||
|
||||
|
||||
class EventHandler(ProcessEvent):
|
||||
def __init__(self, client=None):
|
||||
self.client = client
|
||||
|
||||
def process_IN_MODIFY(self, event):
|
||||
self.client.write_message(f.read().decode('utf-8', 'replace'))
|
||||
|
||||
|
||||
def file_monitor(path='.', client=None):
|
||||
wm = WatchManager()
|
||||
mask = IN_DELETE | IN_CREATE | IN_MODIFY
|
||||
notifier = AsyncNotifier(wm, EventHandler(client))
|
||||
wm.add_watch(path, mask, auto_add=True, rec=True)
|
||||
if not os.path.isfile(path):
|
||||
logger.debug("File %s does not exist." % path)
|
||||
sys.exit(3)
|
||||
else:
|
||||
logger.debug("Now starting monitor file %s." % path)
|
||||
global f
|
||||
f = open(path, 'r')
|
||||
st_size = os.stat(path)[6]
|
||||
f.seek(st_size)
|
||||
|
||||
while True:
|
||||
try:
|
||||
notifier.process_events()
|
||||
if notifier.check_events():
|
||||
notifier.read_events()
|
||||
except KeyboardInterrupt:
|
||||
print "keyboard Interrupt."
|
||||
notifier.stop()
|
||||
break
|
||||
|
||||
|
||||
class MonitorHandler(tornado.websocket.WebSocketHandler):
|
||||
clients = []
|
||||
threads = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.file_path = None
|
||||
super(self.__class__, self).__init__(*args, **kwargs)
|
||||
|
||||
def check_origin(self, origin):
|
||||
return True
|
||||
|
||||
@django_request_support
|
||||
@require_auth('admin')
|
||||
def open(self):
|
||||
# 获取监控的path
|
||||
self.file_path = self.get_argument('file_path', '')
|
||||
MonitorHandler.clients.append(self)
|
||||
thread = MyThread(target=file_monitor, args=('%s.log' % self.file_path, self))
|
||||
MonitorHandler.threads.append(thread)
|
||||
self.stream.set_nodelay(True)
|
||||
|
||||
try:
|
||||
for t in MonitorHandler.threads:
|
||||
if t.is_alive():
|
||||
continue
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
|
||||
except WebSocketClosedError:
|
||||
client_index = MonitorHandler.clients.index(self)
|
||||
MonitorHandler.threads[client_index].stop()
|
||||
MonitorHandler.clients.remove(self)
|
||||
MonitorHandler.threads.remove(MonitorHandler.threads[client_index])
|
||||
|
||||
logger.debug("Websocket: Monitor client num: %s, thread num: %s" % (len(MonitorHandler.clients),
|
||||
len(MonitorHandler.threads)))
|
||||
|
||||
def on_message(self, message):
|
||||
# 监控日志,发生变动发向客户端
|
||||
pass
|
||||
|
||||
def on_close(self):
|
||||
# 客户端主动关闭
|
||||
# self.close()
|
||||
|
||||
logger.debug("Websocket: Monitor client close request")
|
||||
try:
|
||||
client_index = MonitorHandler.clients.index(self)
|
||||
MonitorHandler.clients.remove(self)
|
||||
MonitorHandler.threads.remove(MonitorHandler.threads[client_index])
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
class WebTty(Tty):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WebTty, self).__init__(*args, **kwargs)
|
||||
self.ws = None
|
||||
self.data = ''
|
||||
self.input_mode = False
|
||||
|
||||
|
||||
class WebTerminalKillHandler(tornado.web.RequestHandler):
|
||||
@django_request_support
|
||||
@require_auth('admin')
|
||||
def get(self):
|
||||
ws_id = self.get_argument('id')
|
||||
Log.objects.filter(id=ws_id).update(is_finished=True)
|
||||
for ws in WebTerminalHandler.clients:
|
||||
if ws.id == int(ws_id):
|
||||
logger.debug("Kill log id %s" % ws_id)
|
||||
ws.log.save()
|
||||
ws.close()
|
||||
logger.debug('Websocket: web terminal client num: %s' % len(WebTerminalHandler.clients))
|
||||
|
||||
|
||||
class ExecHandler(tornado.websocket.WebSocketHandler):
|
||||
clients = []
|
||||
tasks = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.id = 0
|
||||
self.user = None
|
||||
self.role = None
|
||||
self.runner = None
|
||||
self.assets = []
|
||||
self.perm = {}
|
||||
self.remote_ip = ''
|
||||
super(ExecHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
def check_origin(self, origin):
|
||||
return True
|
||||
|
||||
@django_request_support
|
||||
@require_auth('user')
|
||||
def open(self):
|
||||
logger.debug('Websocket: Open exec request')
|
||||
role_name = self.get_argument('role', 'sb')
|
||||
self.remote_ip = self.request.headers.get("X-Real-IP")
|
||||
if not self.remote_ip:
|
||||
self.remote_ip = self.request.remote_ip
|
||||
logger.debug('Web执行命令: 请求系统用户 %s' % role_name)
|
||||
self.role = get_object(PermRole, name=role_name)
|
||||
self.perm = get_group_user_perm(self.user)
|
||||
roles = self.perm.get('role').keys()
|
||||
if self.role not in roles:
|
||||
self.write_message('No perm that role %s' % role_name)
|
||||
self.close()
|
||||
self.assets = self.perm.get('role').get(self.role).get('asset')
|
||||
|
||||
res = gen_resource({'user': self.user, 'asset': self.assets, 'role': self.role})
|
||||
self.runner = MyRunner(res)
|
||||
message = '有权限的主机: ' + ', '.join([asset.hostname for asset in self.assets])
|
||||
self.__class__.clients.append(self)
|
||||
self.write_message(message)
|
||||
|
||||
def on_message(self, message):
|
||||
data = json.loads(message)
|
||||
pattern = data.get('pattern', '')
|
||||
self.command = data.get('command', '')
|
||||
self.asset_name_str = ''
|
||||
if pattern and self.command:
|
||||
for inv in self.runner.inventory.get_hosts(pattern=pattern):
|
||||
self.asset_name_str += '%s ' % inv.name
|
||||
self.write_message('匹配主机: ' + self.asset_name_str)
|
||||
self.write_message('<span style="color: yellow">Ansible> %s</span>\n\n' % self.command)
|
||||
self.__class__.tasks.append(MyThread(target=self.run_cmd, args=(self.command, pattern)))
|
||||
|
||||
for t in self.__class__.tasks:
|
||||
if t.is_alive():
|
||||
continue
|
||||
try:
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def run_cmd(self, command, pattern):
|
||||
self.runner.run('shell', command, pattern=pattern)
|
||||
ExecLog(host=self.asset_name_str, cmd=self.command, user=self.user.username,
|
||||
remote_ip=self.remote_ip, result=self.runner.results).save()
|
||||
newline_pattern = re.compile(r'\n')
|
||||
for k, v in self.runner.results.items():
|
||||
for host, output in v.items():
|
||||
output = newline_pattern.sub('<br />', output)
|
||||
if k == 'ok':
|
||||
header = "<span style='color: green'>[ %s => %s]</span>\n" % (host, 'Ok')
|
||||
else:
|
||||
header = "<span style='color: red'>[ %s => %s]</span>\n" % (host, 'failed')
|
||||
self.write_message(header)
|
||||
self.write_message(output)
|
||||
|
||||
self.write_message('\n~o~ Task finished ~o~\n')
|
||||
|
||||
def on_close(self):
|
||||
logger.debug('关闭web_exec请求')
|
||||
|
||||
|
||||
class WebTerminalHandler(tornado.websocket.WebSocketHandler):
|
||||
clients = []
|
||||
tasks = []
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.term = None
|
||||
self.log_file_f = None
|
||||
self.log_time_f = None
|
||||
self.log = None
|
||||
self.id = 0
|
||||
self.user = None
|
||||
self.ssh = None
|
||||
self.channel = None
|
||||
super(WebTerminalHandler, self).__init__(*args, **kwargs)
|
||||
|
||||
def check_origin(self, origin):
|
||||
return True
|
||||
|
||||
@django_request_support
|
||||
@require_auth('user')
|
||||
def open(self):
|
||||
logger.debug('Websocket: Open request')
|
||||
role_name = self.get_argument('role', 'sb')
|
||||
asset_id = self.get_argument('id', 9999)
|
||||
asset = get_object(Asset, id=asset_id)
|
||||
self.termlog = TermLogRecorder(User.objects.get(id=self.user_id))
|
||||
if asset:
|
||||
roles = user_have_perm(self.user, asset)
|
||||
logger.debug(roles)
|
||||
logger.debug('系统用户: %s' % role_name)
|
||||
login_role = ''
|
||||
for role in roles:
|
||||
if role.name == role_name:
|
||||
login_role = role
|
||||
break
|
||||
if not login_role:
|
||||
logger.warning('Websocket: Not that Role %s for Host: %s User: %s ' % (role_name, asset.hostname,
|
||||
self.user.username))
|
||||
self.close()
|
||||
return
|
||||
else:
|
||||
logger.warning('Websocket: No that Host: %s User: %s ' % (asset_id, self.user.username))
|
||||
self.close()
|
||||
return
|
||||
logger.debug('Websocket: request web terminal Host: %s User: %s Role: %s' % (asset.hostname, self.user.username,
|
||||
login_role.name))
|
||||
self.term = WebTty(self.user, asset, login_role, login_type='web')
|
||||
# self.term.remote_ip = self.request.remote_ip
|
||||
self.term.remote_ip = self.request.headers.get("X-Real-IP")
|
||||
if not self.term.remote_ip:
|
||||
self.term.remote_ip = self.request.remote_ip
|
||||
self.ssh = self.term.get_connection()
|
||||
self.channel = self.ssh.invoke_shell(term='xterm')
|
||||
WebTerminalHandler.tasks.append(MyThread(target=self.forward_outbound))
|
||||
WebTerminalHandler.clients.append(self)
|
||||
|
||||
for t in WebTerminalHandler.tasks:
|
||||
if t.is_alive():
|
||||
continue
|
||||
try:
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def on_message(self, message):
|
||||
jsondata = json.loads(message)
|
||||
if not jsondata:
|
||||
return
|
||||
|
||||
if 'resize' in jsondata.get('data'):
|
||||
self.termlog.write(message)
|
||||
self.channel.resize_pty(
|
||||
width=int(jsondata.get('data').get('resize').get('cols', 100)),
|
||||
height=int(jsondata.get('data').get('resize').get('rows', 35))
|
||||
)
|
||||
elif jsondata.get('data'):
|
||||
self.termlog.recoder = True
|
||||
self.term.input_mode = True
|
||||
if str(jsondata['data']) in ['\r', '\n', '\r\n']:
|
||||
match = re.compile(r'\x1b\[\?1049', re.X).findall(self.term.vim_data)
|
||||
if match:
|
||||
if self.term.vim_flag or len(match) == 2:
|
||||
self.term.vim_flag = False
|
||||
else:
|
||||
self.term.vim_flag = True
|
||||
elif not self.term.vim_flag:
|
||||
result = self.term.deal_command(self.term.data)[0:200]
|
||||
if len(result) > 0:
|
||||
TtyLog(log=self.log, datetime=datetime.datetime.now(), cmd=result).save()
|
||||
self.term.vim_data = ''
|
||||
self.term.data = ''
|
||||
self.term.input_mode = False
|
||||
self.channel.send(jsondata['data'])
|
||||
else:
|
||||
pass
|
||||
|
||||
def on_close(self):
|
||||
logger.debug('Websocket: Close request')
|
||||
print self.termlog.CMD
|
||||
self.termlog.save()
|
||||
if self in WebTerminalHandler.clients:
|
||||
WebTerminalHandler.clients.remove(self)
|
||||
try:
|
||||
self.log_file_f.write('End time is %s' % datetime.datetime.now())
|
||||
self.log.is_finished = True
|
||||
self.log.end_time = datetime.datetime.now()
|
||||
self.log.filename = self.termlog.filename
|
||||
self.log.save()
|
||||
self.log_time_f.close()
|
||||
self.ssh.close()
|
||||
self.close()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def forward_outbound(self):
|
||||
self.log_file_f, self.log_time_f, self.log = self.term.get_log()
|
||||
self.id = self.log.id
|
||||
self.termlog.setid(self.id)
|
||||
try:
|
||||
data = ''
|
||||
pre_timestamp = time.time()
|
||||
while True:
|
||||
r, w, e = select.select([self.channel, sys.stdin], [], [])
|
||||
if self.channel in r:
|
||||
recv = self.channel.recv(1024)
|
||||
if not len(recv):
|
||||
return
|
||||
data += recv
|
||||
self.term.vim_data += recv
|
||||
try:
|
||||
self.write_message(data.decode('utf-8', 'replace'))
|
||||
self.termlog.write(data)
|
||||
self.termlog.recoder = False
|
||||
now_timestamp = time.time()
|
||||
self.log_time_f.write('%s %s\n' % (round(now_timestamp - pre_timestamp, 4), len(data)))
|
||||
self.log_file_f.write(data)
|
||||
pre_timestamp = now_timestamp
|
||||
self.log_file_f.flush()
|
||||
self.log_time_f.flush()
|
||||
if self.term.input_mode:
|
||||
self.term.data += data
|
||||
data = ''
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
# class MonitorHandler(WebTerminalHandler):
|
||||
# @django_request_support
|
||||
# @require_auth('user')
|
||||
# def open(self):
|
||||
# try:
|
||||
# self.returnlog = TermLogRecorder.loglist[self.get_argument('id')]
|
||||
# self.returnlog.write_message = self.write_message
|
||||
# except:
|
||||
# self.write_message('Log is None')
|
||||
# self.close()
|
||||
#
|
||||
# def on_message(self, message):
|
||||
# pass
|
||||
#
|
||||
# def on_close(self):
|
||||
# self.close()
|
||||
|
||||
|
||||
class Application(tornado.web.Application):
|
||||
def __init__(self):
|
||||
handlers = [
|
||||
(r'/monitor', MonitorHandler),
|
||||
(r'/terminal', WebTerminalHandler),
|
||||
(r'/kill', WebTerminalKillHandler),
|
||||
(r'/exec', ExecHandler),
|
||||
]
|
||||
|
||||
setting = {
|
||||
'cookie_secret': 'DFksdfsasdfkasdfFKwlwfsdfsa1204mx',
|
||||
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
|
||||
'debug': False,
|
||||
}
|
||||
|
||||
tornado.web.Application.__init__(self, handlers, **setting)
|
||||
|
||||
|
||||
def main():
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
import tornado.wsgi
|
||||
wsgi_app = get_wsgi_application()
|
||||
container = tornado.wsgi.WSGIContainer(wsgi_app)
|
||||
setting = {
|
||||
'cookie_secret': 'DFksdfsasdfkasdfFKwlwfsdfsa1204mx',
|
||||
'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
|
||||
'static_path': os.path.join(os.path.dirname(__file__), 'static'),
|
||||
'debug': False,
|
||||
}
|
||||
tornado_app = tornado.web.Application(
|
||||
[
|
||||
(r'/ws/monitor', MonitorHandler),
|
||||
(r'/ws/terminal', WebTerminalHandler),
|
||||
(r'/kill', WebTerminalKillHandler),
|
||||
(r'/ws/exec', ExecHandler),
|
||||
(r"/static/(.*)", tornado.web.StaticFileHandler,
|
||||
dict(path=os.path.join(os.path.dirname(__file__), "static"))),
|
||||
('.*', tornado.web.FallbackHandler, dict(fallback=container)),
|
||||
], **setting)
|
||||
|
||||
server = tornado.httpserver.HTTPServer(tornado_app)
|
||||
server.listen(options.port, address=IP)
|
||||
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# tornado.options.parse_command_line()
|
||||
# app = Application()
|
||||
# server = tornado.httpserver.HTTPServer(app)
|
||||
# server.bind(options.port, options.host)
|
||||
# #server.listen(options.port)
|
||||
# server.start(num_processes=5)
|
||||
# tornado.ioloop.IOLoop.instance().start()
|
||||
print "Run server on %s:%s" % (options.host, options.port)
|
||||
main()
|
115
service.sh
|
@ -1,115 +0,0 @@
|
|||
#!/bin/bash
|
||||
# jumpserver Startup script for the jumpserver Server
|
||||
#
|
||||
# chkconfig: - 85 12
|
||||
# description: Open source detecting system
|
||||
# processname: jumpserver
|
||||
# Date: 2016-02-27
|
||||
# Version: 3.0.1
|
||||
# Site: http://www.jumpserver.org
|
||||
# Author: Jumpserver Team
|
||||
|
||||
jumpserver_dir=
|
||||
|
||||
base_dir=$(dirname $0)
|
||||
jumpserver_dir=${jumpserver_dir:-$base_dir}
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
|
||||
|
||||
if [ -f ${jumpserver_dir}/install/functions ];then
|
||||
. ${jumpserver_dir}/install/functions
|
||||
elif [ -f /etc/init.d/functions ];then
|
||||
. /etc/init.d/functions
|
||||
else
|
||||
echo "No functions script found in [./functions, ./install/functions, /etc/init.d/functions]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PROC_NAME="jumpserver"
|
||||
lockfile=/var/lock/subsys/${PROC_NAME}
|
||||
|
||||
start() {
|
||||
jump_start=$"Starting ${PROC_NAME} service:"
|
||||
if [ -f $lockfile ];then
|
||||
echo -n "jumpserver is running..."
|
||||
success "$jump_start"
|
||||
echo
|
||||
else
|
||||
daemon python $jumpserver_dir/manage.py crontab add &>> /var/log/jumpserver.log 2>&1
|
||||
daemon python $jumpserver_dir/run_server.py &> /dev/null 2>&1 &
|
||||
sleep 1
|
||||
echo -n "$jump_start"
|
||||
ps axu | grep 'run_server' | grep -v 'grep' &> /dev/null
|
||||
if [ $? == '0' ];then
|
||||
success "$jump_start"
|
||||
if [ ! -e $lockfile ]; then
|
||||
lockfile_dir=`dirname $lockfile`
|
||||
mkdir -pv $lockfile_dir
|
||||
fi
|
||||
touch "$lockfile"
|
||||
echo
|
||||
else
|
||||
failure "$jump_start"
|
||||
echo
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
stop() {
|
||||
echo -n $"Stopping ${PROC_NAME} service:"
|
||||
daemon python $jumpserver_dir/manage.py crontab remove &>> /var/log/jumpserver.log 2>&1
|
||||
ps aux | grep -E 'run_server.py' | grep -v grep | awk '{print $2}' | xargs kill -9 &> /dev/null
|
||||
ret=$?
|
||||
if [ $ret -eq 0 ]; then
|
||||
echo_success
|
||||
echo
|
||||
rm -f "$lockfile"
|
||||
else
|
||||
echo_failure
|
||||
echo
|
||||
rm -f "$lockfile"
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
status(){
|
||||
ps axu | grep 'run_server' | grep -v 'grep' &> /dev/null
|
||||
if [ $? == '0' ];then
|
||||
echo -n "jumpserver is running..."
|
||||
success
|
||||
touch "$lockfile"
|
||||
echo
|
||||
else
|
||||
echo -n "jumpserver is not running."
|
||||
failure
|
||||
echo
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
|
||||
restart(){
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
# See how we were called.
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
|
||||
restart)
|
||||
restart
|
||||
;;
|
||||
|
||||
status)
|
||||
status
|
||||
;;
|
||||
*)
|
||||
echo $"Usage: $0 {start|stop|restart|status}"
|
||||
exit 2
|
||||
esac
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
Colorbox Core Style:
|
||||
The following CSS is consistent between example themes and should not be altered.
|
||||
*/
|
||||
#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
|
||||
#cboxWrapper {max-width:none;}
|
||||
#cboxOverlay{position:fixed; width:100%; height:100%;}
|
||||
#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
|
||||
#cboxContent{position:relative;}
|
||||
#cboxLoadedContent{overflow:auto; -webkit-overflow-scrolling: touch;}
|
||||
#cboxTitle{margin:0;}
|
||||
#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%; height:100%;}
|
||||
#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
|
||||
.cboxPhoto{float:left; margin:auto; border:0; display:block; max-width:none; -ms-interpolation-mode:bicubic;}
|
||||
.cboxIframe{width:100%; height:100%; display:block; border:0; padding:0; margin:0;}
|
||||
#colorbox, #cboxContent, #cboxLoadedContent{box-sizing:content-box; -moz-box-sizing:content-box; -webkit-box-sizing:content-box;}
|
||||
|
||||
/*
|
||||
User Style:
|
||||
Change the following styles to modify the appearance of Colorbox. They are
|
||||
ordered & tabbed in a way that represents the nesting of the generated HTML.
|
||||
*/
|
||||
#cboxOverlay{background:#fff; opacity: 0.9; filter: alpha(opacity = 90);}
|
||||
#colorbox{outline:0;}
|
||||
#cboxContent{margin-top:32px; overflow:visible; background:#000;}
|
||||
.cboxIframe{background:#fff;}
|
||||
#cboxError{padding:50px; border:1px solid #ccc;}
|
||||
#cboxLoadedContent{background:#000; padding:1px;}
|
||||
#cboxLoadingGraphic{background:url(images/loading.gif) no-repeat center center;}
|
||||
#cboxLoadingOverlay{background:#000;}
|
||||
#cboxTitle{position:absolute; top:-22px; left:0; color:#000;}
|
||||
#cboxCurrent{position:absolute; top:-22px; right:205px; text-indent:-9999px;}
|
||||
|
||||
/* these elements are buttons, and may need to have additional styles reset to avoid unwanted base styles */
|
||||
#cboxPrevious, #cboxNext, #cboxSlideshow, #cboxClose {border:0; padding:0; margin:0; overflow:visible; text-indent:-9999px; width:20px; height:20px; position:absolute; top:-20px; background:url(images/controls.png) no-repeat 0 0;}
|
||||
|
||||
/* avoid outlines on :active (mouseclick), but preserve outlines on :focus (tabbed navigating) */
|
||||
#cboxPrevious:active, #cboxNext:active, #cboxSlideshow:active, #cboxClose:active {outline:0;}
|
||||
|
||||
#cboxPrevious{background-position:0px 0px; right:44px;}
|
||||
#cboxPrevious:hover{background-position:0px -25px;}
|
||||
#cboxNext{background-position:-25px 0px; right:22px;}
|
||||
#cboxNext:hover{background-position:-25px -25px;}
|
||||
#cboxClose{background-position:-50px 0px; right:0;}
|
||||
#cboxClose:hover{background-position:-50px -25px;}
|
||||
.cboxSlideshow_on #cboxPrevious, .cboxSlideshow_off #cboxPrevious{right:66px;}
|
||||
.cboxSlideshow_on #cboxSlideshow{background-position:-75px -25px; right:44px;}
|
||||
.cboxSlideshow_on #cboxSlideshow:hover{background-position:-100px -25px;}
|
||||
.cboxSlideshow_off #cboxSlideshow{background-position:-100px 0px; right:44px;}
|
||||
.cboxSlideshow_off #cboxSlideshow:hover{background-position:-75px -25px;}
|
Before Width: | Height: | Size: 503 B |
Before Width: | Height: | Size: 6.1 KiB |
|
@ -1,374 +0,0 @@
|
|||
/* Magnific Popup CSS */
|
||||
.mfp-bg {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1042;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
background: #0b0b0b;
|
||||
opacity: 0.8;
|
||||
filter: alpha(opacity=80); }
|
||||
|
||||
.mfp-wrap {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1043;
|
||||
position: fixed;
|
||||
outline: none !important;
|
||||
-webkit-backface-visibility: hidden; }
|
||||
|
||||
.mfp-container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 0 8px;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box; }
|
||||
|
||||
.mfp-container:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle; }
|
||||
|
||||
.mfp-align-top .mfp-container:before {
|
||||
display: none; }
|
||||
|
||||
.mfp-content {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0 auto;
|
||||
text-align: left;
|
||||
z-index: 1045; }
|
||||
|
||||
.mfp-inline-holder .mfp-content, .mfp-ajax-holder .mfp-content {
|
||||
width: 100%;
|
||||
cursor: auto; }
|
||||
|
||||
.mfp-ajax-cur {
|
||||
cursor: progress; }
|
||||
|
||||
.mfp-zoom-out-cur, .mfp-zoom-out-cur .mfp-image-holder .mfp-close {
|
||||
cursor: -moz-zoom-out;
|
||||
cursor: -webkit-zoom-out;
|
||||
cursor: zoom-out; }
|
||||
|
||||
.mfp-zoom {
|
||||
cursor: pointer;
|
||||
cursor: -webkit-zoom-in;
|
||||
cursor: -moz-zoom-in;
|
||||
cursor: zoom-in; }
|
||||
|
||||
.mfp-auto-cursor .mfp-content {
|
||||
cursor: auto; }
|
||||
|
||||
.mfp-close, .mfp-arrow, .mfp-preloader, .mfp-counter {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none; }
|
||||
|
||||
.mfp-loading.mfp-figure {
|
||||
display: none; }
|
||||
|
||||
.mfp-hide {
|
||||
display: none !important; }
|
||||
|
||||
.mfp-preloader {
|
||||
color: #CCC;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
margin-top: -0.8em;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
z-index: 1044; }
|
||||
.mfp-preloader a {
|
||||
color: #CCC; }
|
||||
.mfp-preloader a:hover {
|
||||
color: #FFF; }
|
||||
|
||||
.mfp-s-ready .mfp-preloader {
|
||||
display: none; }
|
||||
|
||||
.mfp-s-error .mfp-content {
|
||||
display: none; }
|
||||
|
||||
button.mfp-close, button.mfp-arrow {
|
||||
overflow: visible;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
z-index: 1046;
|
||||
-webkit-box-shadow: none;
|
||||
box-shadow: none; }
|
||||
button::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border: 0; }
|
||||
|
||||
.mfp-close {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
opacity: 0.65;
|
||||
filter: alpha(opacity=65);
|
||||
padding: 0 0 18px 10px;
|
||||
color: #FFF;
|
||||
font-style: normal;
|
||||
font-size: 28px;
|
||||
font-family: Arial, Baskerville, monospace; }
|
||||
.mfp-close:hover, .mfp-close:focus {
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100); }
|
||||
.mfp-close:active {
|
||||
top: 1px; }
|
||||
|
||||
.mfp-close-btn-in .mfp-close {
|
||||
color: #333; }
|
||||
|
||||
.mfp-image-holder .mfp-close, .mfp-iframe-holder .mfp-close {
|
||||
color: #FFF;
|
||||
right: -6px;
|
||||
text-align: right;
|
||||
padding-right: 6px;
|
||||
width: 100%; }
|
||||
|
||||
.mfp-counter {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #CCC;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap; }
|
||||
|
||||
.mfp-arrow {
|
||||
position: absolute;
|
||||
opacity: 0.65;
|
||||
filter: alpha(opacity=65);
|
||||
margin: 0;
|
||||
top: 50%;
|
||||
margin-top: -55px;
|
||||
padding: 0;
|
||||
width: 90px;
|
||||
height: 110px;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
|
||||
.mfp-arrow:active {
|
||||
margin-top: -54px; }
|
||||
.mfp-arrow:hover, .mfp-arrow:focus {
|
||||
opacity: 1;
|
||||
filter: alpha(opacity=100); }
|
||||
.mfp-arrow:before, .mfp-arrow:after, .mfp-arrow .mfp-b, .mfp-arrow .mfp-a {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin-top: 35px;
|
||||
margin-left: 35px;
|
||||
border: medium inset transparent; }
|
||||
.mfp-arrow:after, .mfp-arrow .mfp-a {
|
||||
border-top-width: 13px;
|
||||
border-bottom-width: 13px;
|
||||
top: 8px; }
|
||||
.mfp-arrow:before, .mfp-arrow .mfp-b {
|
||||
border-top-width: 21px;
|
||||
border-bottom-width: 21px;
|
||||
opacity: 0.7; }
|
||||
|
||||
.mfp-arrow-left {
|
||||
left: 0; }
|
||||
.mfp-arrow-left:after, .mfp-arrow-left .mfp-a {
|
||||
border-right: 17px solid #FFF;
|
||||
margin-left: 31px; }
|
||||
.mfp-arrow-left:before, .mfp-arrow-left .mfp-b {
|
||||
margin-left: 25px;
|
||||
border-right: 27px solid #3F3F3F; }
|
||||
|
||||
.mfp-arrow-right {
|
||||
right: 0; }
|
||||
.mfp-arrow-right:after, .mfp-arrow-right .mfp-a {
|
||||
border-left: 17px solid #FFF;
|
||||
margin-left: 39px; }
|
||||
.mfp-arrow-right:before, .mfp-arrow-right .mfp-b {
|
||||
border-left: 27px solid #3F3F3F; }
|
||||
|
||||
.mfp-iframe-holder {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px; }
|
||||
.mfp-iframe-holder .mfp-content {
|
||||
line-height: 0;
|
||||
width: 100%;
|
||||
max-width: 900px; }
|
||||
.mfp-iframe-holder .mfp-close {
|
||||
top: -40px; }
|
||||
|
||||
.mfp-iframe-scaler {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 56.25%; }
|
||||
.mfp-iframe-scaler iframe {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||
background: #000; }
|
||||
|
||||
/* Main image in popup */
|
||||
img.mfp-img {
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
line-height: 0;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 40px 0 40px;
|
||||
margin: 0 auto; }
|
||||
|
||||
/* The shadow behind the image */
|
||||
.mfp-figure {
|
||||
line-height: 0; }
|
||||
.mfp-figure:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 40px;
|
||||
bottom: 40px;
|
||||
display: block;
|
||||
right: 0;
|
||||
width: auto;
|
||||
height: auto;
|
||||
z-index: -1;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.6);
|
||||
background: #444; }
|
||||
.mfp-figure small {
|
||||
color: #BDBDBD;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 14px; }
|
||||
.mfp-figure figure {
|
||||
margin: 0; }
|
||||
|
||||
.mfp-bottom-bar {
|
||||
margin-top: -36px;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
cursor: auto; }
|
||||
|
||||
.mfp-title {
|
||||
text-align: left;
|
||||
line-height: 18px;
|
||||
color: #F3F3F3;
|
||||
word-wrap: break-word;
|
||||
padding-right: 36px; }
|
||||
|
||||
.mfp-image-holder .mfp-content {
|
||||
max-width: 100%; }
|
||||
|
||||
.mfp-gallery .mfp-image-holder .mfp-figure {
|
||||
cursor: pointer; }
|
||||
|
||||
@media screen and (max-width: 800px) and (orientation: landscape), screen and (max-height: 300px) {
|
||||
/**
|
||||
* Remove all paddings around the image on small screen
|
||||
*/
|
||||
.mfp-img-mobile .mfp-image-holder {
|
||||
padding-left: 0;
|
||||
padding-right: 0; }
|
||||
.mfp-img-mobile img.mfp-img {
|
||||
padding: 0; }
|
||||
.mfp-img-mobile .mfp-figure:after {
|
||||
top: 0;
|
||||
bottom: 0; }
|
||||
.mfp-img-mobile .mfp-figure small {
|
||||
display: inline;
|
||||
margin-left: 5px; }
|
||||
.mfp-img-mobile .mfp-bottom-bar {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
top: auto;
|
||||
padding: 3px 5px;
|
||||
position: fixed;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box; }
|
||||
.mfp-img-mobile .mfp-bottom-bar:empty {
|
||||
padding: 0; }
|
||||
.mfp-img-mobile .mfp-counter {
|
||||
right: 5px;
|
||||
top: 3px; }
|
||||
.mfp-img-mobile .mfp-close {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
position: fixed;
|
||||
text-align: center;
|
||||
padding: 0; }
|
||||
}
|
||||
|
||||
@media all and (max-width: 900px) {
|
||||
.mfp-arrow {
|
||||
-webkit-transform: scale(0.75);
|
||||
transform: scale(0.75); }
|
||||
|
||||
.mfp-arrow-left {
|
||||
-webkit-transform-origin: 0;
|
||||
transform-origin: 0; }
|
||||
|
||||
.mfp-arrow-right {
|
||||
-webkit-transform-origin: 100%;
|
||||
transform-origin: 100%; }
|
||||
|
||||
.mfp-container {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px; }
|
||||
}
|
||||
|
||||
.mfp-ie7 .mfp-img {
|
||||
padding: 0; }
|
||||
.mfp-ie7 .mfp-bottom-bar {
|
||||
width: 600px;
|
||||
left: 50%;
|
||||
margin-left: -300px;
|
||||
margin-top: 5px;
|
||||
padding-bottom: 5px; }
|
||||
.mfp-ie7 .mfp-container {
|
||||
padding: 0; }
|
||||
.mfp-ie7 .mfp-content {
|
||||
padding-top: 44px; }
|
||||
.mfp-ie7 .mfp-close {
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding-top: 0; }
|
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 210 B |
|
@ -1,689 +0,0 @@
|
|||
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
|
||||
audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
|
||||
audio:not([controls]){display:none;}
|
||||
html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
|
||||
a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
|
||||
a:hover,a:active{outline:0;}
|
||||
sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
|
||||
sup{top:-0.5em;}
|
||||
sub{bottom:-0.25em;}
|
||||
img{height:auto;border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;}
|
||||
button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
|
||||
button,input{*overflow:visible;line-height:normal;}
|
||||
button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
|
||||
button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
|
||||
input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
|
||||
input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
|
||||
textarea{overflow:auto;vertical-align:top;}
|
||||
.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";}
|
||||
.clearfix:after{clear:both;}
|
||||
.hide-text{overflow:hidden;text-indent:100%;white-space:nowrap;}
|
||||
.input-block-level{display:block;width:100%;min-height:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;}
|
||||
body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;}
|
||||
a{color:#0088cc;text-decoration:none;}
|
||||
a:hover{color:#005580;text-decoration:underline;}
|
||||
.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
|
||||
.row:after{clear:both;}
|
||||
[class*="span"]{float:left;margin-left:20px;}
|
||||
.container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
|
||||
.span12{width:940px;}
|
||||
.span11{width:860px;}
|
||||
.span10{width:780px;}
|
||||
.span9{width:700px;}
|
||||
.span8{width:620px;}
|
||||
.span7{width:540px;}
|
||||
.span6{width:460px;}
|
||||
.span5{width:380px;}
|
||||
.span4{width:300px;}
|
||||
.span3{width:220px;}
|
||||
.span2{width:140px;}
|
||||
.span1{width:60px;}
|
||||
.offset12{margin-left:980px;}
|
||||
.offset11{margin-left:900px;}
|
||||
.offset10{margin-left:820px;}
|
||||
.offset9{margin-left:740px;}
|
||||
.offset8{margin-left:660px;}
|
||||
.offset7{margin-left:580px;}
|
||||
.offset6{margin-left:500px;}
|
||||
.offset5{margin-left:420px;}
|
||||
.offset4{margin-left:340px;}
|
||||
.offset3{margin-left:260px;}
|
||||
.offset2{margin-left:180px;}
|
||||
.offset1{margin-left:100px;}
|
||||
.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
|
||||
.row-fluid:after{clear:both;}
|
||||
.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
|
||||
.row-fluid>[class*="span"]:first-child{margin-left:0;}
|
||||
.row-fluid > .span12{width:99.99999998999999%;}
|
||||
.row-fluid > .span11{width:91.489361693%;}
|
||||
.row-fluid > .span10{width:82.97872339599999%;}
|
||||
.row-fluid > .span9{width:74.468085099%;}
|
||||
.row-fluid > .span8{width:65.95744680199999%;}
|
||||
.row-fluid > .span7{width:57.446808505%;}
|
||||
.row-fluid > .span6{width:48.93617020799999%;}
|
||||
.row-fluid > .span5{width:40.425531911%;}
|
||||
.row-fluid > .span4{width:31.914893614%;}
|
||||
.row-fluid > .span3{width:23.404255317%;}
|
||||
.row-fluid > .span2{width:14.89361702%;}
|
||||
.row-fluid > .span1{width:6.382978723%;}
|
||||
.container{margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
|
||||
.container:after{clear:both;}
|
||||
.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
|
||||
.container-fluid:after{clear:both;}
|
||||
p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;}
|
||||
.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;}
|
||||
h1,h2,h3,h4,h5,h6{margin:0;font-family:inherit;font-weight:bold;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
|
||||
h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;}
|
||||
h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;}
|
||||
h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;}
|
||||
h4,h5,h6{line-height:18px;}
|
||||
h4{font-size:14px;}h4 small{font-size:12px;}
|
||||
h5{font-size:12px;}
|
||||
h6{font-size:11px;color:#999999;text-transform:uppercase;}
|
||||
.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;}
|
||||
.page-header h1{line-height:1;}
|
||||
ul,ol{padding:0;margin:0 0 9px 25px;}
|
||||
ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
|
||||
ul{list-style:disc;}
|
||||
ol{list-style:decimal;}
|
||||
li{line-height:18px;}
|
||||
ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
|
||||
dl{margin-bottom:18px;}
|
||||
dt,dd{line-height:18px;}
|
||||
dt{font-weight:bold;line-height:17px;}
|
||||
dd{margin-left:9px;}
|
||||
.dl-horizontal dt{float:left;clear:left;width:120px;text-align:right;}
|
||||
.dl-horizontal dd{margin-left:130px;}
|
||||
hr{margin:18px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
|
||||
strong{font-weight:bold;}
|
||||
em{font-style:italic;}
|
||||
.muted{color:#999999;}
|
||||
abbr[title]{border-bottom:1px dotted #ddd;cursor:help;}
|
||||
abbr.initialism{font-size:90%;text-transform:uppercase;}
|
||||
blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;}
|
||||
blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
|
||||
blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
|
||||
q:before,q:after,blockquote:before,blockquote:after{content:"";}
|
||||
address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;}
|
||||
small{font-size:100%;}
|
||||
cite{font-style:normal;}
|
||||
code,pre{padding:0 3px 2px;font-family:Menlo,Monaco,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
|
||||
code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
|
||||
pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12.025px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;word-wrap:break-word;}pre.prettyprint{margin-bottom:18px;}
|
||||
pre code{padding:0;color:inherit;background-color:transparent;border:0;}
|
||||
.pre-scrollable{max-height:340px;overflow-y:scroll;}
|
||||
form{margin:0 0 18px;}
|
||||
fieldset{padding:0;margin:0;border:0;}
|
||||
legend{display:block;width:100%;padding:0;margin-bottom:27px;font-size:19.5px;line-height:36px;color:#333333;border:0;border-bottom:1px solid #eee;}legend small{font-size:13.5px;color:#999999;}
|
||||
label,input,button,select,textarea{font-size:13px;font-weight:normal;line-height:18px;}
|
||||
input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
|
||||
label{display:block;margin-bottom:5px;color:#333333;}
|
||||
input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;margin-bottom:9px;font-size:13px;line-height:18px;color:#555555;border:1px solid #cccccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
|
||||
.uneditable-textarea{width:auto;height:auto;}
|
||||
label input,label textarea,label select{display:block;}
|
||||
input[type="image"],input[type="checkbox"],input[type="radio"]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;cursor:pointer;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border:0 \9;}
|
||||
input[type="image"]{border:0;}
|
||||
input[type="file"]{width:auto;padding:initial;line-height:initial;border:initial;background-color:#ffffff;background-color:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
|
||||
input[type="button"],input[type="reset"],input[type="submit"]{width:auto;height:auto;}
|
||||
select,input[type="file"]{height:28px;*margin-top:4px;line-height:28px;}
|
||||
input[type="file"]{line-height:18px \9;}
|
||||
select{width:220px;background-color:#ffffff;}
|
||||
select[multiple],select[size]{height:auto;}
|
||||
input[type="image"]{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
|
||||
textarea{height:auto;}
|
||||
input[type="hidden"]{display:none;}
|
||||
.radio,.checkbox{padding-left:18px;}
|
||||
.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
|
||||
.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
|
||||
.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
|
||||
.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
|
||||
input,textarea{-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;}
|
||||
input:focus,textarea:focus{border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075),0 0 8px rgba(82, 168, 236, 0.6);outline:0;outline:thin dotted \9;}
|
||||
input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
|
||||
.input-mini{width:60px;}
|
||||
.input-small{width:90px;}
|
||||
.input-medium{width:150px;}
|
||||
.input-large{width:210px;}
|
||||
.input-xlarge{width:270px;}
|
||||
.input-xxlarge{width:530px;}
|
||||
input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{float:none;margin-left:0;}
|
||||
input,textarea,.uneditable-input{margin-left:0;}
|
||||
input.span12, textarea.span12, .uneditable-input.span12{width:930px;}
|
||||
input.span11, textarea.span11, .uneditable-input.span11{width:850px;}
|
||||
input.span10, textarea.span10, .uneditable-input.span10{width:770px;}
|
||||
input.span9, textarea.span9, .uneditable-input.span9{width:690px;}
|
||||
input.span8, textarea.span8, .uneditable-input.span8{width:610px;}
|
||||
input.span7, textarea.span7, .uneditable-input.span7{width:530px;}
|
||||
input.span6, textarea.span6, .uneditable-input.span6{width:450px;}
|
||||
input.span5, textarea.span5, .uneditable-input.span5{width:370px;}
|
||||
input.span4, textarea.span4, .uneditable-input.span4{width:290px;}
|
||||
input.span3, textarea.span3, .uneditable-input.span3{width:210px;}
|
||||
input.span2, textarea.span2, .uneditable-input.span2{width:130px;}
|
||||
input.span1, textarea.span1, .uneditable-input.span1{width:50px;}
|
||||
input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#eeeeee;border-color:#ddd;cursor:not-allowed;}
|
||||
.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
|
||||
.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;border-color:#c09853;}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:0 0 6px #dbc59e;-moz-box-shadow:0 0 6px #dbc59e;box-shadow:0 0 6px #dbc59e;}
|
||||
.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
|
||||
.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
|
||||
.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;border-color:#b94a48;}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:0 0 6px #d59392;-moz-box-shadow:0 0 6px #d59392;box-shadow:0 0 6px #d59392;}
|
||||
.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
|
||||
.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
|
||||
.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;border-color:#468847;}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:0 0 6px #7aba7b;-moz-box-shadow:0 0 6px #7aba7b;box-shadow:0 0 6px #7aba7b;}
|
||||
.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
|
||||
input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
|
||||
.form-actions{padding:17px 20px 18px;margin-top:18px;margin-bottom:18px;background-color:#eeeeee;border-top:1px solid #ddd;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";}
|
||||
.form-actions:after{clear:both;}
|
||||
.uneditable-input{display:block;background-color:#ffffff;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
|
||||
:-moz-placeholder{color:#999999;}
|
||||
::-webkit-input-placeholder{color:#999999;}
|
||||
.help-block,.help-inline{color:#555555;}
|
||||
.help-block{display:block;margin-bottom:9px;}
|
||||
.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
|
||||
.input-prepend,.input-append{margin-bottom:5px;}.input-prepend input,.input-append input,.input-prepend select,.input-append select,.input-prepend .uneditable-input,.input-append .uneditable-input{*margin-left:0;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-prepend input:focus,.input-append input:focus,.input-prepend select:focus,.input-append select:focus,.input-prepend .uneditable-input:focus,.input-append .uneditable-input:focus{position:relative;z-index:2;}
|
||||
.input-prepend .uneditable-input,.input-append .uneditable-input{border-left-color:#ccc;}
|
||||
.input-prepend .add-on,.input-append .add-on{display:inline-block;width:auto;min-width:16px;height:18px;padding:4px 5px;font-weight:normal;line-height:18px;text-align:center;text-shadow:0 1px 0 #ffffff;vertical-align:middle;background-color:#eeeeee;border:1px solid #ccc;}
|
||||
.input-prepend .add-on,.input-append .add-on,.input-prepend .btn,.input-append .btn{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
|
||||
.input-prepend .active,.input-append .active{background-color:#a9dba9;border-color:#46a546;}
|
||||
.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
|
||||
.input-append input,.input-append select .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
|
||||
.input-append .uneditable-input{border-left-color:#eee;border-right-color:#ccc;}
|
||||
.input-append .add-on,.input-append .btn{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
|
||||
.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
|
||||
.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
|
||||
.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
|
||||
.search-query{padding-left:14px;padding-right:14px;margin-bottom:0;-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;}
|
||||
.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;margin-bottom:0;}
|
||||
.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
|
||||
.form-search label,.form-inline label{display:inline-block;}
|
||||
.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
|
||||
.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
|
||||
.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-left:0;margin-right:3px;}
|
||||
.control-group{margin-bottom:9px;}
|
||||
legend+.control-group{margin-top:18px;-webkit-margin-top-collapse:separate;}
|
||||
.form-horizontal .control-group{margin-bottom:18px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";}
|
||||
.form-horizontal .control-group:after{clear:both;}
|
||||
.form-horizontal .control-label{float:left;width:140px;padding-top:5px;text-align:right;}
|
||||
.form-horizontal .controls{margin-left:160px;*display:inline-block;*margin-left:0;*padding-left:20px;}
|
||||
.form-horizontal .help-block{margin-top:9px;margin-bottom:0;}
|
||||
.form-horizontal .form-actions{padding-left:160px;}
|
||||
table{max-width:100%;border-collapse:collapse;border-spacing:0;background-color:transparent;}
|
||||
.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:8px;line-height:18px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
|
||||
.table th{font-weight:bold;}
|
||||
.table thead th{vertical-align:bottom;}
|
||||
.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
|
||||
.table tbody+tbody{border-top:2px solid #dddddd;}
|
||||
.table-condensed th,.table-condensed td{padding:4px 5px;}
|
||||
.table-bordered{border:1px solid #dddddd;border-left:0;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
|
||||
.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
|
||||
.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
|
||||
.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
|
||||
.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
|
||||
.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
|
||||
.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
|
||||
.table tbody tr:hover td,.table tbody tr:hover th{background-color:#f5f5f5;}
|
||||
table .span1{float:none;width:44px;margin-left:0;}
|
||||
table .span2{float:none;width:124px;margin-left:0;}
|
||||
table .span3{float:none;width:204px;margin-left:0;}
|
||||
table .span4{float:none;width:284px;margin-left:0;}
|
||||
table .span5{float:none;width:364px;margin-left:0;}
|
||||
table .span6{float:none;width:444px;margin-left:0;}
|
||||
table .span7{float:none;width:524px;margin-left:0;}
|
||||
table .span8{float:none;width:604px;margin-left:0;}
|
||||
table .span9{float:none;width:684px;margin-left:0;}
|
||||
table .span10{float:none;width:764px;margin-left:0;}
|
||||
table .span11{float:none;width:844px;margin-left:0;}
|
||||
table .span12{float:none;width:924px;margin-left:0;}
|
||||
table .span13{float:none;width:1004px;margin-left:0;}
|
||||
table .span14{float:none;width:1084px;margin-left:0;}
|
||||
table .span15{float:none;width:1164px;margin-left:0;}
|
||||
table .span16{float:none;width:1244px;margin-left:0;}
|
||||
table .span17{float:none;width:1324px;margin-left:0;}
|
||||
table .span18{float:none;width:1404px;margin-left:0;}
|
||||
table .span19{float:none;width:1484px;margin-left:0;}
|
||||
table .span20{float:none;width:1564px;margin-left:0;}
|
||||
table .span21{float:none;width:1644px;margin-left:0;}
|
||||
table .span22{float:none;width:1724px;margin-left:0;}
|
||||
table .span23{float:none;width:1804px;margin-left:0;}
|
||||
table .span24{float:none;width:1884px;margin-left:0;}
|
||||
[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;*margin-right:.3em;}[class^="icon-"]:last-child,[class*=" icon-"]:last-child{*margin-left:0;}
|
||||
.icon-white{background-image:url("../img/glyphicons-halflings-white.png");}
|
||||
.icon-glass{background-position:0 0;}
|
||||
.icon-music{background-position:-24px 0;}
|
||||
.icon-search{background-position:-48px 0;}
|
||||
.icon-envelope{background-position:-72px 0;}
|
||||
.icon-heart{background-position:-96px 0;}
|
||||
.icon-star{background-position:-120px 0;}
|
||||
.icon-star-empty{background-position:-144px 0;}
|
||||
.icon-user{background-position:-168px 0;}
|
||||
.icon-film{background-position:-192px 0;}
|
||||
.icon-th-large{background-position:-216px 0;}
|
||||
.icon-th{background-position:-240px 0;}
|
||||
.icon-th-list{background-position:-264px 0;}
|
||||
.icon-ok{background-position:-288px 0;}
|
||||
.icon-remove{background-position:-312px 0;}
|
||||
.icon-zoom-in{background-position:-336px 0;}
|
||||
.icon-zoom-out{background-position:-360px 0;}
|
||||
.icon-off{background-position:-384px 0;}
|
||||
.icon-signal{background-position:-408px 0;}
|
||||
.icon-cog{background-position:-432px 0;}
|
||||
.icon-trash{background-position:-456px 0;}
|
||||
.icon-home{background-position:0 -24px;}
|
||||
.icon-file{background-position:-24px -24px;}
|
||||
.icon-time{background-position:-48px -24px;}
|
||||
.icon-road{background-position:-72px -24px;}
|
||||
.icon-download-alt{background-position:-96px -24px;}
|
||||
.icon-download{background-position:-120px -24px;}
|
||||
.icon-upload{background-position:-144px -24px;}
|
||||
.icon-inbox{background-position:-168px -24px;}
|
||||
.icon-play-circle{background-position:-192px -24px;}
|
||||
.icon-repeat{background-position:-216px -24px;}
|
||||
.icon-refresh{background-position:-240px -24px;}
|
||||
.icon-list-alt{background-position:-264px -24px;}
|
||||
.icon-lock{background-position:-287px -24px;}
|
||||
.icon-flag{background-position:-312px -24px;}
|
||||
.icon-headphones{background-position:-336px -24px;}
|
||||
.icon-volume-off{background-position:-360px -24px;}
|
||||
.icon-volume-down{background-position:-384px -24px;}
|
||||
.icon-volume-up{background-position:-408px -24px;}
|
||||
.icon-qrcode{background-position:-432px -24px;}
|
||||
.icon-barcode{background-position:-456px -24px;}
|
||||
.icon-tag{background-position:0 -48px;}
|
||||
.icon-tags{background-position:-25px -48px;}
|
||||
.icon-book{background-position:-48px -48px;}
|
||||
.icon-bookmark{background-position:-72px -48px;}
|
||||
.icon-print{background-position:-96px -48px;}
|
||||
.icon-camera{background-position:-120px -48px;}
|
||||
.icon-font{background-position:-144px -48px;}
|
||||
.icon-bold{background-position:-167px -48px;}
|
||||
.icon-italic{background-position:-192px -48px;}
|
||||
.icon-text-height{background-position:-216px -48px;}
|
||||
.icon-text-width{background-position:-240px -48px;}
|
||||
.icon-align-left{background-position:-264px -48px;}
|
||||
.icon-align-center{background-position:-288px -48px;}
|
||||
.icon-align-right{background-position:-312px -48px;}
|
||||
.icon-align-justify{background-position:-336px -48px;}
|
||||
.icon-list{background-position:-360px -48px;}
|
||||
.icon-indent-left{background-position:-384px -48px;}
|
||||
.icon-indent-right{background-position:-408px -48px;}
|
||||
.icon-facetime-video{background-position:-432px -48px;}
|
||||
.icon-picture{background-position:-456px -48px;}
|
||||
.icon-pencil{background-position:0 -72px;}
|
||||
.icon-map-marker{background-position:-24px -72px;}
|
||||
.icon-adjust{background-position:-48px -72px;}
|
||||
.icon-tint{background-position:-72px -72px;}
|
||||
.icon-edit{background-position:-96px -72px;}
|
||||
.icon-share{background-position:-120px -72px;}
|
||||
.icon-check{background-position:-144px -72px;}
|
||||
.icon-move{background-position:-168px -72px;}
|
||||
.icon-step-backward{background-position:-192px -72px;}
|
||||
.icon-fast-backward{background-position:-216px -72px;}
|
||||
.icon-backward{background-position:-240px -72px;}
|
||||
.icon-play{background-position:-264px -72px;}
|
||||
.icon-pause{background-position:-288px -72px;}
|
||||
.icon-stop{background-position:-312px -72px;}
|
||||
.icon-forward{background-position:-336px -72px;}
|
||||
.icon-fast-forward{background-position:-360px -72px;}
|
||||
.icon-step-forward{background-position:-384px -72px;}
|
||||
.icon-eject{background-position:-408px -72px;}
|
||||
.icon-chevron-left{background-position:-432px -72px;}
|
||||
.icon-chevron-right{background-position:-456px -72px;}
|
||||
.icon-plus-sign{background-position:0 -96px;}
|
||||
.icon-minus-sign{background-position:-24px -96px;}
|
||||
.icon-remove-sign{background-position:-48px -96px;}
|
||||
.icon-ok-sign{background-position:-72px -96px;}
|
||||
.icon-question-sign{background-position:-96px -96px;}
|
||||
.icon-info-sign{background-position:-120px -96px;}
|
||||
.icon-screenshot{background-position:-144px -96px;}
|
||||
.icon-remove-circle{background-position:-168px -96px;}
|
||||
.icon-ok-circle{background-position:-192px -96px;}
|
||||
.icon-ban-circle{background-position:-216px -96px;}
|
||||
.icon-arrow-left{background-position:-240px -96px;}
|
||||
.icon-arrow-right{background-position:-264px -96px;}
|
||||
.icon-arrow-up{background-position:-289px -96px;}
|
||||
.icon-arrow-down{background-position:-312px -96px;}
|
||||
.icon-share-alt{background-position:-336px -96px;}
|
||||
.icon-resize-full{background-position:-360px -96px;}
|
||||
.icon-resize-small{background-position:-384px -96px;}
|
||||
.icon-plus{background-position:-408px -96px;}
|
||||
.icon-minus{background-position:-433px -96px;}
|
||||
.icon-asterisk{background-position:-456px -96px;}
|
||||
.icon-exclamation-sign{background-position:0 -120px;}
|
||||
.icon-gift{background-position:-24px -120px;}
|
||||
.icon-leaf{background-position:-48px -120px;}
|
||||
.icon-fire{background-position:-72px -120px;}
|
||||
.icon-eye-open{background-position:-96px -120px;}
|
||||
.icon-eye-close{background-position:-120px -120px;}
|
||||
.icon-warning-sign{background-position:-144px -120px;}
|
||||
.icon-plane{background-position:-168px -120px;}
|
||||
.icon-calendar{background-position:-192px -120px;}
|
||||
.icon-random{background-position:-216px -120px;}
|
||||
.icon-comment{background-position:-240px -120px;}
|
||||
.icon-magnet{background-position:-264px -120px;}
|
||||
.icon-chevron-up{background-position:-288px -120px;}
|
||||
.icon-chevron-down{background-position:-313px -119px;}
|
||||
.icon-retweet{background-position:-336px -120px;}
|
||||
.icon-shopping-cart{background-position:-360px -120px;}
|
||||
.icon-folder-close{background-position:-384px -120px;}
|
||||
.icon-folder-open{background-position:-408px -120px;}
|
||||
.icon-resize-vertical{background-position:-432px -119px;}
|
||||
.icon-resize-horizontal{background-position:-456px -118px;}
|
||||
.dropdown{position:relative;}
|
||||
.dropdown-toggle{*margin-bottom:-3px;}
|
||||
.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
|
||||
.caret{display:inline-block;width:0;height:0;vertical-align:top;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000000;opacity:0.3;filter:alpha(opacity=30);content:"";}
|
||||
.dropdown .caret{margin-top:8px;margin-left:2px;}
|
||||
.dropdown:hover .caret,.open.dropdown .caret{opacity:1;filter:alpha(opacity=100);}
|
||||
.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;float:left;display:none;min-width:160px;padding:4px 0;margin:0;list-style:none;background-color:#ffffff;border-color:#ccc;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:1px;-webkit-border-radius:0 0 5px 5px;-moz-border-radius:0 0 5px 5px;border-radius:0 0 5px 5px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;*border-right-width:2px;*border-bottom-width:2px;}.dropdown-menu.pull-right{right:0;left:auto;}
|
||||
.dropdown-menu .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
|
||||
.dropdown-menu a{display:block;padding:3px 15px;clear:both;font-weight:normal;line-height:18px;color:#333333;white-space:nowrap;}
|
||||
.dropdown-menu li>a:hover,.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;background-color:#0088cc;}
|
||||
.dropdown.open{*z-index:1000;}.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
|
||||
.dropdown.open .dropdown-menu{display:block;}
|
||||
.pull-right .dropdown-menu{left:auto;right:0;}
|
||||
.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"\2191";}
|
||||
.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
|
||||
.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
|
||||
.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
|
||||
.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
|
||||
.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
|
||||
.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
|
||||
.collapse{-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-ms-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;position:relative;overflow:hidden;height:0;}.collapse.in{height:auto;}
|
||||
.close{float:right;font-size:20px;font-weight:bold;line-height:18px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;opacity:0.4;filter:alpha(opacity=40);cursor:pointer;}
|
||||
.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 10px 4px;margin-bottom:0;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);vertical-align:middle;background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-ms-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(top, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);border:1px solid #cccccc;border-bottom-color:#b3b3b3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{background-color:#e6e6e6;}
|
||||
.btn:active,.btn.active{background-color:#cccccc \9;}
|
||||
.btn:first-child{*margin-left:0;}
|
||||
.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
|
||||
.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
|
||||
.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;outline:0;}
|
||||
.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
|
||||
.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
|
||||
.btn-large [class^="icon-"]{margin-top:1px;}
|
||||
.btn-small{padding:5px 9px;font-size:11px;line-height:16px;}
|
||||
.btn-small [class^="icon-"]{margin-top:-1px;}
|
||||
.btn-mini{padding:2px 6px;font-size:11px;line-height:14px;}
|
||||
.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover,.btn-inverse,.btn-inverse:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
|
||||
.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
|
||||
.btn-primary{background-color:#0074cc;background-image:-moz-linear-gradient(top, #0088cc, #0055cc);background-image:-ms-linear-gradient(top, #0088cc, #0055cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0055cc));background-image:-webkit-linear-gradient(top, #0088cc, #0055cc);background-image:-o-linear-gradient(top, #0088cc, #0055cc);background-image:linear-gradient(top, #0088cc, #0055cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0055cc', GradientType=0);border-color:#0055cc #0055cc #003580;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0055cc;}
|
||||
.btn-primary:active,.btn-primary.active{background-color:#004099 \9;}
|
||||
.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;}
|
||||
.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
|
||||
.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;}
|
||||
.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
|
||||
.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;}
|
||||
.btn-success:active,.btn-success.active{background-color:#408140 \9;}
|
||||
.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;}
|
||||
.btn-info:active,.btn-info.active{background-color:#24748c \9;}
|
||||
.btn-inverse{background-color:#414141;background-image:-moz-linear-gradient(top, #555555, #222222);background-image:-ms-linear-gradient(top, #555555, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#555555), to(#222222));background-image:-webkit-linear-gradient(top, #555555, #222222);background-image:-o-linear-gradient(top, #555555, #222222);background-image:linear-gradient(top, #555555, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#555555', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{background-color:#222222;}
|
||||
.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
|
||||
button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
|
||||
button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
|
||||
button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
|
||||
button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
|
||||
.btn-group{position:relative;*zoom:1;*margin-left:.3em;}.btn-group:before,.btn-group:after{display:table;content:"";}
|
||||
.btn-group:after{clear:both;}
|
||||
.btn-group:first-child{*margin-left:0;}
|
||||
.btn-group+.btn-group{margin-left:5px;}
|
||||
.btn-toolbar{margin-top:9px;margin-bottom:9px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
|
||||
.btn-group .btn{position:relative;float:left;margin-left:-1px;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
|
||||
.btn-group .btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
|
||||
.btn-group .btn:last-child,.btn-group .dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
|
||||
.btn-group .btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
|
||||
.btn-group .btn.large:last-child,.btn-group .large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
|
||||
.btn-group .btn:hover,.btn-group .btn:focus,.btn-group .btn:active,.btn-group .btn.active{z-index:2;}
|
||||
.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
|
||||
.btn-group .dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125),inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:3px;*padding-bottom:3px;}
|
||||
.btn-group .btn-mini.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:1px;*padding-bottom:1px;}
|
||||
.btn-group .btn-small.dropdown-toggle{*padding-top:4px;*padding-bottom:4px;}
|
||||
.btn-group .btn-large.dropdown-toggle{padding-left:12px;padding-right:12px;}
|
||||
.btn-group.open{*z-index:1000;}.btn-group.open .dropdown-menu{display:block;margin-top:1px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
|
||||
.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 6px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);}
|
||||
.btn .caret{margin-top:7px;margin-left:0;}
|
||||
.btn:hover .caret,.open.btn-group .caret{opacity:1;filter:alpha(opacity=100);}
|
||||
.btn-mini .caret{margin-top:5px;}
|
||||
.btn-small .caret{margin-top:6px;}
|
||||
.btn-large .caret{margin-top:6px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
|
||||
.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:0.75;filter:alpha(opacity=75);}
|
||||
.alert{padding:8px 35px 8px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
|
||||
.alert-heading{color:inherit;}
|
||||
.alert .close{position:relative;top:-2px;right:-21px;line-height:18px;}
|
||||
.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
|
||||
.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
|
||||
.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
|
||||
.alert-block{padding-top:14px;padding-bottom:14px;}
|
||||
.alert-block>p,.alert-block>ul{margin-bottom:0;}
|
||||
.alert-block p+p{margin-top:5px;}
|
||||
.nav{margin-left:0;margin-bottom:18px;list-style:none;}
|
||||
.nav>li>a{display:block;}
|
||||
.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
|
||||
.nav .nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
|
||||
.nav li+.nav-header{margin-top:9px;}
|
||||
.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
|
||||
.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
|
||||
.nav-list>li>a{padding:3px 15px;}
|
||||
.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
|
||||
.nav-list [class^="icon-"]{margin-right:2px;}
|
||||
.nav-list .divider{height:1px;margin:8px 1px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;*width:100%;*margin:-5px 0 5px;}
|
||||
.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
|
||||
.nav-tabs:after,.nav-pills:after{clear:both;}
|
||||
.nav-tabs>li,.nav-pills>li{float:left;}
|
||||
.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
|
||||
.nav-tabs{border-bottom:1px solid #ddd;}
|
||||
.nav-tabs>li{margin-bottom:-1px;}
|
||||
.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:18px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
|
||||
.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
|
||||
.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
|
||||
.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;}
|
||||
.nav-stacked>li{float:none;}
|
||||
.nav-stacked>li>a{margin-right:0;}
|
||||
.nav-tabs.nav-stacked{border-bottom:0;}
|
||||
.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
|
||||
.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
|
||||
.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
|
||||
.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
|
||||
.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
|
||||
.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
|
||||
.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
|
||||
.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
|
||||
.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;}
|
||||
.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;}
|
||||
.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;border-bottom-color:#333333;}
|
||||
.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
|
||||
.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
|
||||
.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
|
||||
.tabs-stacked .open>a:hover{border-color:#999999;}
|
||||
.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
|
||||
.tabbable:after{clear:both;}
|
||||
.tab-content{display:table;width:100%;}
|
||||
.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
|
||||
.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
|
||||
.tab-content>.active,.pill-content>.active{display:block;}
|
||||
.tabs-below .nav-tabs{border-top:1px solid #ddd;}
|
||||
.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
|
||||
.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
|
||||
.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
|
||||
.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
|
||||
.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
|
||||
.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
|
||||
.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
|
||||
.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
|
||||
.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
|
||||
.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
|
||||
.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
|
||||
.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
|
||||
.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
|
||||
.navbar{*position:relative;*z-index:2;overflow:visible;margin-bottom:18px;}
|
||||
.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
|
||||
.navbar .container{width:auto;}
|
||||
.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:dximagetransform.microsoft.gradient(enabled=false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;}
|
||||
.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;}
|
||||
.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
|
||||
.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
|
||||
.nav-collapse.collapse{height:auto;}
|
||||
.navbar{color:#999999;}.navbar .brand:hover{text-decoration:none;}
|
||||
.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
|
||||
.navbar .navbar-text{margin-bottom:0;line-height:40px;}
|
||||
.navbar .btn,.navbar .btn-group{margin-top:5px;}
|
||||
.navbar .btn-group .btn{margin-top:0;}
|
||||
.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
|
||||
.navbar-form:after{clear:both;}
|
||||
.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
|
||||
.navbar-form input,.navbar-form select{display:inline-block;margin-bottom:0;}
|
||||
.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
|
||||
.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
|
||||
.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;background-color:#626262;border:1px solid #151515;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query:-moz-placeholder{color:#cccccc;}
|
||||
.navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
|
||||
.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
|
||||
.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
|
||||
.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
|
||||
.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
|
||||
.navbar-fixed-top{top:0;}
|
||||
.navbar-fixed-bottom{bottom:0;}
|
||||
.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
|
||||
.navbar .nav.pull-right{float:right;}
|
||||
.navbar .nav>li{display:block;float:left;}
|
||||
.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
|
||||
.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
|
||||
.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;}
|
||||
.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;}
|
||||
.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
|
||||
.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
|
||||
.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
|
||||
.navbar-fixed-bottom .dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
|
||||
.navbar-fixed-bottom .dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
|
||||
.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
|
||||
.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
|
||||
.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
|
||||
.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
|
||||
.navbar .nav.pull-right .dropdown-menu,.navbar .nav .dropdown-menu.pull-right{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before,.navbar .nav .dropdown-menu.pull-right:before{left:auto;right:12px;}
|
||||
.navbar .nav.pull-right .dropdown-menu:after,.navbar .nav .dropdown-menu.pull-right:after{left:auto;right:13px;}
|
||||
.breadcrumb{padding:7px 14px;margin:0 0 18px;list-style:none;background-color:#fbfbfb;background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
|
||||
.breadcrumb .divider{padding:0 5px;color:#999999;}
|
||||
.breadcrumb .active a{color:#333333;}
|
||||
.pagination{height:36px;margin:18px 0;}
|
||||
.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
|
||||
.pagination li{display:inline;}
|
||||
.pagination a{float:left;padding:0 14px;line-height:34px;text-decoration:none;border:1px solid #ddd;border-left-width:0;}
|
||||
.pagination a:hover,.pagination .active a{background-color:#f5f5f5;}
|
||||
.pagination .active a{color:#999999;cursor:default;}
|
||||
.pagination .disabled span,.pagination .disabled a,.pagination .disabled a:hover{color:#999999;background-color:transparent;cursor:default;}
|
||||
.pagination li:first-child a{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
|
||||
.pagination li:last-child a{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
|
||||
.pagination-centered{text-align:center;}
|
||||
.pagination-right{text-align:right;}
|
||||
.pager{margin-left:0;margin-bottom:18px;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";}
|
||||
.pager:after{clear:both;}
|
||||
.pager li{display:inline;}
|
||||
.pager a{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
|
||||
.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
|
||||
.pager .next a{float:right;}
|
||||
.pager .previous a{float:left;}
|
||||
.pager .disabled a,.pager .disabled a:hover{color:#999999;background-color:#fff;cursor:default;}
|
||||
.modal-open .dropdown-menu{z-index:2050;}
|
||||
.modal-open .dropdown.open{*z-index:2050;}
|
||||
.modal-open .popover{z-index:2060;}
|
||||
.modal-open .tooltip{z-index:2070;}
|
||||
.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
|
||||
.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
|
||||
.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
|
||||
.modal.fade.in{top:50%;}
|
||||
.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
|
||||
.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
|
||||
.modal-form{margin-bottom:0;}
|
||||
.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";}
|
||||
.modal-footer:after{clear:both;}
|
||||
.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
|
||||
.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
|
||||
.tooltip{position:absolute;z-index:1020;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
|
||||
.tooltip.top{margin-top:-2px;}
|
||||
.tooltip.right{margin-left:2px;}
|
||||
.tooltip.bottom{margin-top:2px;}
|
||||
.tooltip.left{margin-left:-2px;}
|
||||
.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
|
||||
.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
|
||||
.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
|
||||
.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
|
||||
.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
|
||||
.tooltip-arrow{position:absolute;width:0;height:0;}
|
||||
.popover{position:absolute;top:0;left:0;z-index:1010;display:none;padding:5px;}.popover.top{margin-top:-5px;}
|
||||
.popover.right{margin-left:5px;}
|
||||
.popover.bottom{margin-top:5px;}
|
||||
.popover.left{margin-left:-5px;}
|
||||
.popover.top .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
|
||||
.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
|
||||
.popover.bottom .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
|
||||
.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
|
||||
.popover .arrow{position:absolute;width:0;height:0;}
|
||||
.popover-inner{padding:3px;width:280px;overflow:hidden;background:#000000;background:rgba(0, 0, 0, 0.8);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
|
||||
.popover-title{padding:9px 15px;line-height:1;background-color:#f5f5f5;border-bottom:1px solid #eee;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;}
|
||||
.popover-content{padding:14px;background-color:#ffffff;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
|
||||
.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";}
|
||||
.thumbnails:after{clear:both;}
|
||||
.thumbnails>li{float:left;margin:0 0 18px 20px;}
|
||||
.thumbnail{display:block;padding:4px;line-height:1;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}
|
||||
a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
|
||||
.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
|
||||
.thumbnail .caption{padding:9px;}
|
||||
.label{padding:1px 4px 2px;font-size:10.998px;font-weight:bold;line-height:13px;color:#ffffff;vertical-align:middle;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
|
||||
.label:hover{color:#ffffff;text-decoration:none;}
|
||||
.label-important{background-color:#b94a48;}
|
||||
.label-important:hover{background-color:#953b39;}
|
||||
.label-warning{background-color:#f89406;}
|
||||
.label-warning:hover{background-color:#c67605;}
|
||||
.label-success{background-color:#468847;}
|
||||
.label-success:hover{background-color:#356635;}
|
||||
.label-info{background-color:#3a87ad;}
|
||||
.label-info:hover{background-color:#2d6987;}
|
||||
.label-inverse{background-color:#333333;}
|
||||
.label-inverse:hover{background-color:#1a1a1a;}
|
||||
.badge{padding:1px 9px 2px;font-size:12.025px;font-weight:bold;white-space:nowrap;color:#ffffff;background-color:#999999;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
|
||||
.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
|
||||
.badge-error{background-color:#b94a48;}
|
||||
.badge-error:hover{background-color:#953b39;}
|
||||
.badge-warning{background-color:#f89406;}
|
||||
.badge-warning:hover{background-color:#c67605;}
|
||||
.badge-success{background-color:#468847;}
|
||||
.badge-success:hover{background-color:#356635;}
|
||||
.badge-info{background-color:#3a87ad;}
|
||||
.badge-info:hover{background-color:#2d6987;}
|
||||
.badge-inverse{background-color:#333333;}
|
||||
.badge-inverse:hover{background-color:#1a1a1a;}
|
||||
@-webkit-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}.progress{overflow:hidden;height:18px;margin-bottom:18px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-ms-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(top, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
|
||||
.progress .bar{width:0%;height:18px;color:#ffffff;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-ms-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(top, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-ms-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
|
||||
.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
|
||||
.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
|
||||
.progress-danger .bar{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);}
|
||||
.progress-danger.progress-striped .bar{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
|
||||
.progress-success .bar{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);}
|
||||
.progress-success.progress-striped .bar{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
|
||||
.progress-info .bar{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);}
|
||||
.progress-info.progress-striped .bar{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
|
||||
.progress-warning .bar{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);}
|
||||
.progress-warning.progress-striped .bar{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
|
||||
.accordion{margin-bottom:18px;}
|
||||
.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
|
||||
.accordion-heading{border-bottom:0;}
|
||||
.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
|
||||
.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
|
||||
.carousel{position:relative;margin-bottom:18px;line-height:1;}
|
||||
.carousel-inner{overflow:hidden;width:100%;position:relative;}
|
||||
.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-ms-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
|
||||
.carousel .item>img{display:block;line-height:1;}
|
||||
.carousel .active,.carousel .next,.carousel .prev{display:block;}
|
||||
.carousel .active{left:0;}
|
||||
.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
|
||||
.carousel .next{left:100%;}
|
||||
.carousel .prev{left:-100%;}
|
||||
.carousel .next.left,.carousel .prev.right{left:0;}
|
||||
.carousel .active.left{left:-100%;}
|
||||
.carousel .active.right{left:100%;}
|
||||
.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
|
||||
.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
|
||||
.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:10px 15px 5px;background:#333333;background:rgba(0, 0, 0, 0.75);}
|
||||
.carousel-caption h4,.carousel-caption p{color:#ffffff;}
|
||||
.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
|
||||
.hero-unit p{font-size:18px;font-weight:200;line-height:27px;color:inherit;}
|
||||
.pull-right{float:right;}
|
||||
.pull-left{float:left;}
|
||||
.hide{display:none;}
|
||||
.show{display:block;}
|
||||
.invisible{visibility:hidden;}
|
Before Width: | Height: | Size: 646 B |
Before Width: | Height: | Size: 872 B |
|
@ -1,429 +0,0 @@
|
|||
/*!
|
||||
Chosen, a Select Box Enhancer for jQuery and Prototype
|
||||
by Patrick Filler for Harvest, http://getharvest.com
|
||||
|
||||
Version 1.1.0
|
||||
Full source at https://github.com/harvesthq/chosen
|
||||
Copyright (c) 2011 Harvest http://getharvest.com
|
||||
|
||||
MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
|
||||
This file is generated by `grunt build`, do not edit it by hand.
|
||||
*/
|
||||
|
||||
/* @group Base */
|
||||
.chosen-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
font-size: 13px;
|
||||
zoom: 1;
|
||||
*display: inline;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.chosen-container .chosen-drop {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -9999px;
|
||||
z-index: 1010;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 1px solid #aaa;
|
||||
border-top: 0;
|
||||
background: #fff;
|
||||
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.chosen-container.chosen-with-drop .chosen-drop {
|
||||
left: 0;
|
||||
}
|
||||
.chosen-container a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Single Chosen */
|
||||
.chosen-container-single .chosen-single {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
padding: 0 0 0 8px;
|
||||
height: 23px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
|
||||
background: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
|
||||
background: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
|
||||
background: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
|
||||
background: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1);
|
||||
color: #444;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
line-height: 24px;
|
||||
}
|
||||
.chosen-container-single .chosen-default {
|
||||
color: #999;
|
||||
}
|
||||
.chosen-container-single .chosen-single span {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
margin-right: 26px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chosen-container-single .chosen-single-with-deselect span {
|
||||
margin-right: 38px;
|
||||
}
|
||||
.chosen-container-single .chosen-single abbr {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 26px;
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url('chosen-sprite.png') -42px 1px no-repeat;
|
||||
font-size: 1px;
|
||||
}
|
||||
.chosen-container-single .chosen-single abbr:hover {
|
||||
background-position: -42px -10px;
|
||||
}
|
||||
.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
|
||||
background-position: -42px -10px;
|
||||
}
|
||||
.chosen-container-single .chosen-single div {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
}
|
||||
.chosen-container-single .chosen-single div b {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url('chosen-sprite.png') no-repeat 0px 2px;
|
||||
}
|
||||
.chosen-container-single .chosen-search {
|
||||
position: relative;
|
||||
z-index: 1010;
|
||||
margin: 0;
|
||||
padding: 3px 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chosen-container-single .chosen-search input[type="text"] {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0;
|
||||
padding: 4px 20px 4px 5px;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
outline: 0;
|
||||
border: 1px solid #aaa;
|
||||
background: white url('chosen-sprite.png') no-repeat 100% -20px;
|
||||
background: url('chosen-sprite.png') no-repeat 100% -20px;
|
||||
font-size: 1em;
|
||||
font-family: sans-serif;
|
||||
line-height: normal;
|
||||
border-radius: 0;
|
||||
}
|
||||
.chosen-container-single .chosen-drop {
|
||||
margin-top: -1px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
.chosen-container-single.chosen-container-single-nosearch .chosen-search {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Results */
|
||||
.chosen-container .chosen-results {
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
margin: 0 4px 4px 0;
|
||||
padding: 0 0 0 4px;
|
||||
max-height: 240px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.chosen-container .chosen-results li {
|
||||
display: none;
|
||||
margin: 0;
|
||||
padding: 5px 6px;
|
||||
list-style: none;
|
||||
line-height: 15px;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
.chosen-container .chosen-results li.active-result {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
.chosen-container .chosen-results li.disabled-result {
|
||||
display: list-item;
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
.chosen-container .chosen-results li.highlighted {
|
||||
background-color: #3875d7;
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
|
||||
background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%);
|
||||
background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%);
|
||||
background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%);
|
||||
background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
|
||||
color: #fff;
|
||||
}
|
||||
.chosen-container .chosen-results li.no-results {
|
||||
display: list-item;
|
||||
background: #f4f4f4;
|
||||
}
|
||||
.chosen-container .chosen-results li.group-result {
|
||||
display: list-item;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
}
|
||||
.chosen-container .chosen-results li.group-option {
|
||||
padding-left: 15px;
|
||||
}
|
||||
.chosen-container .chosen-results li em {
|
||||
font-style: normal;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Multi Chosen */
|
||||
.chosen-container-multi .chosen-choices {
|
||||
-moz-box-sizing: border-box;
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #CBD5DD;
|
||||
border-radius: 2px;
|
||||
cursor: text;
|
||||
height: auto !important;
|
||||
margin: 0;
|
||||
min-height: 30px;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li {
|
||||
float: left;
|
||||
list-style: none;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-field {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
|
||||
margin: 1px 0;
|
||||
padding: 5px;
|
||||
height: 25px;
|
||||
outline: 0;
|
||||
border: 0 !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none;
|
||||
color: #666;
|
||||
font-size: 100%;
|
||||
font-family: sans-serif;
|
||||
line-height: normal;
|
||||
border-radius: 0;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-field .default {
|
||||
color: #999;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice {
|
||||
position: relative;
|
||||
margin: 3px 0 3px 5px;
|
||||
padding: 3px 20px 3px 5px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 3px;
|
||||
background-color: #e4e4e4;
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
|
||||
background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-clip: padding-box;
|
||||
box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05);
|
||||
color: #333;
|
||||
line-height: 13px;
|
||||
cursor: default;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 3px;
|
||||
display: block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: url('chosen-sprite.png') -42px 1px no-repeat;
|
||||
font-size: 1px;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
|
||||
background-position: -42px -10px;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice-disabled {
|
||||
padding-right: 5px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #e4e4e4;
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
|
||||
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
|
||||
color: #666;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice-focus {
|
||||
background: #d4d4d4;
|
||||
}
|
||||
.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
|
||||
background-position: -42px -10px;
|
||||
}
|
||||
.chosen-container-multi .chosen-results {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.chosen-container-multi .chosen-drop .result-selected {
|
||||
display: list-item;
|
||||
color: #ccc;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Active */
|
||||
.chosen-container-active .chosen-single {
|
||||
border: 1px solid #5897fb;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.chosen-container-active.chosen-with-drop .chosen-single {
|
||||
border: 1px solid #aaa;
|
||||
-moz-border-radius-bottomright: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
-moz-border-radius-bottomleft: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
|
||||
background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%);
|
||||
background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%);
|
||||
background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%);
|
||||
background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
|
||||
box-shadow: 0 1px 0 #fff inset;
|
||||
}
|
||||
.chosen-container-active.chosen-with-drop .chosen-single div {
|
||||
border-left: none;
|
||||
background: transparent;
|
||||
}
|
||||
.chosen-container-active.chosen-with-drop .chosen-single div b {
|
||||
background-position: -18px 2px;
|
||||
}
|
||||
.chosen-container-active .chosen-choices {
|
||||
border: 1px solid #5897fb;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.chosen-container-active .chosen-choices li.search-field input[type="text"] {
|
||||
color: #111 !important;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Disabled Support */
|
||||
.chosen-disabled {
|
||||
opacity: 0.5 !important;
|
||||
cursor: default;
|
||||
}
|
||||
.chosen-disabled .chosen-single {
|
||||
cursor: default;
|
||||
}
|
||||
.chosen-disabled .chosen-choices .search-choice .search-choice-close {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Right to Left */
|
||||
.chosen-rtl {
|
||||
text-align: right;
|
||||
}
|
||||
.chosen-rtl .chosen-single {
|
||||
overflow: visible;
|
||||
padding: 0 8px 0 0;
|
||||
}
|
||||
.chosen-rtl .chosen-single span {
|
||||
margin-right: 0;
|
||||
margin-left: 26px;
|
||||
direction: rtl;
|
||||
}
|
||||
.chosen-rtl .chosen-single-with-deselect span {
|
||||
margin-left: 38px;
|
||||
}
|
||||
.chosen-rtl .chosen-single div {
|
||||
right: auto;
|
||||
left: 3px;
|
||||
}
|
||||
.chosen-rtl .chosen-single abbr {
|
||||
right: auto;
|
||||
left: 26px;
|
||||
}
|
||||
.chosen-rtl .chosen-choices li {
|
||||
float: right;
|
||||
}
|
||||
.chosen-rtl .chosen-choices li.search-field input[type="text"] {
|
||||
direction: rtl;
|
||||
}
|
||||
.chosen-rtl .chosen-choices li.search-choice {
|
||||
margin: 3px 5px 3px 0;
|
||||
padding: 3px 5px 3px 19px;
|
||||
}
|
||||
.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
|
||||
right: auto;
|
||||
left: 4px;
|
||||
}
|
||||
.chosen-rtl.chosen-container-single-nosearch .chosen-search,
|
||||
.chosen-rtl .chosen-drop {
|
||||
left: 9999px;
|
||||
}
|
||||
.chosen-rtl.chosen-container-single .chosen-results {
|
||||
margin: 0 0 4px 4px;
|
||||
padding: 0 4px 0 0;
|
||||
}
|
||||
.chosen-rtl .chosen-results li.group-option {
|
||||
padding-right: 15px;
|
||||
padding-left: 0;
|
||||
}
|
||||
.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
|
||||
border-right: none;
|
||||
}
|
||||
.chosen-rtl .chosen-search input[type="text"] {
|
||||
padding: 4px 5px 4px 20px;
|
||||
background: white url('chosen-sprite.png') no-repeat -30px -20px;
|
||||
background: url('chosen-sprite.png') no-repeat -30px -20px;
|
||||
direction: rtl;
|
||||
}
|
||||
.chosen-rtl.chosen-container-single .chosen-single div b {
|
||||
background-position: 6px 2px;
|
||||
}
|
||||
.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
|
||||
background-position: -12px 2px;
|
||||
}
|
||||
|
||||
/* @end */
|
||||
/* @group Retina compatibility */
|
||||
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) {
|
||||
.chosen-rtl .chosen-search input[type="text"],
|
||||
.chosen-container-single .chosen-single abbr,
|
||||
.chosen-container-single .chosen-single div b,
|
||||
.chosen-container-single .chosen-search input[type="text"],
|
||||
.chosen-container-multi .chosen-choices .search-choice .search-choice-close,
|
||||
.chosen-container .chosen-results-scroll-down span,
|
||||
.chosen-container .chosen-results-scroll-up span {
|
||||
background-image: url('chosen-sprite@2x.png') !important;
|
||||
background-size: 52px 37px !important;
|
||||
background-repeat: no-repeat !important;
|
||||
}
|
||||
}
|
||||
/* @end */
|
|
@ -1,789 +0,0 @@
|
|||
/*!
|
||||
* Datepicker for Bootstrap
|
||||
*
|
||||
* Copyright 2012 Stefan Petre
|
||||
* Improvements by Andrew Rowls
|
||||
* Licensed under the Apache License v2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*/
|
||||
.datepicker {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
direction: ltr;
|
||||
/*.dow {
|
||||
border-top: 1px solid #ddd !important;
|
||||
}*/
|
||||
}
|
||||
.datepicker-inline {
|
||||
width: 220px;
|
||||
}
|
||||
.datepicker.datepicker-rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
.datepicker.datepicker-rtl table tr td span {
|
||||
float: right;
|
||||
}
|
||||
.datepicker-dropdown {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.datepicker-dropdown:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 7px solid transparent;
|
||||
border-right: 7px solid transparent;
|
||||
border-bottom: 7px solid #ccc;
|
||||
border-top: 0;
|
||||
border-bottom-color: rgba(0, 0, 0, 0.2);
|
||||
position: absolute;
|
||||
}
|
||||
.datepicker-dropdown:after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
border-bottom: 6px solid #fff;
|
||||
border-top: 0;
|
||||
position: absolute;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-left:before {
|
||||
left: 6px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-left:after {
|
||||
left: 7px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-right:before {
|
||||
right: 6px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-right:after {
|
||||
right: 7px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-top:before {
|
||||
top: -7px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-top:after {
|
||||
top: -6px;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-bottom:before {
|
||||
bottom: -7px;
|
||||
border-bottom: 0;
|
||||
border-top: 7px solid #999;
|
||||
}
|
||||
.datepicker-dropdown.datepicker-orient-bottom:after {
|
||||
bottom: -6px;
|
||||
border-bottom: 0;
|
||||
border-top: 6px solid #fff;
|
||||
}
|
||||
.datepicker > div {
|
||||
display: none;
|
||||
}
|
||||
.datepicker.days div.datepicker-days {
|
||||
display: block;
|
||||
}
|
||||
.datepicker.months div.datepicker-months {
|
||||
display: block;
|
||||
}
|
||||
.datepicker.years div.datepicker-years {
|
||||
display: block;
|
||||
}
|
||||
.datepicker table {
|
||||
margin: 0;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
.datepicker table tr td,
|
||||
.datepicker table tr th {
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
.table-striped .datepicker table tr td,
|
||||
.table-striped .datepicker table tr th {
|
||||
background-color: transparent;
|
||||
}
|
||||
.datepicker table tr td.day:hover,
|
||||
.datepicker table tr td.day.focused {
|
||||
background: #eeeeee;
|
||||
cursor: pointer;
|
||||
}
|
||||
.datepicker table tr td.old,
|
||||
.datepicker table tr td.new {
|
||||
color: #999999;
|
||||
}
|
||||
.datepicker table tr td.disabled,
|
||||
.datepicker table tr td.disabled:hover {
|
||||
background: none;
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
}
|
||||
.datepicker table tr td.today,
|
||||
.datepicker table tr td.today:hover,
|
||||
.datepicker table tr td.today.disabled,
|
||||
.datepicker table tr td.today.disabled:hover {
|
||||
color: #000000;
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
.datepicker table tr td.today:hover,
|
||||
.datepicker table tr td.today:hover:hover,
|
||||
.datepicker table tr td.today.disabled:hover,
|
||||
.datepicker table tr td.today.disabled:hover:hover,
|
||||
.datepicker table tr td.today:focus,
|
||||
.datepicker table tr td.today:hover:focus,
|
||||
.datepicker table tr td.today.disabled:focus,
|
||||
.datepicker table tr td.today.disabled:hover:focus,
|
||||
.datepicker table tr td.today:active,
|
||||
.datepicker table tr td.today:hover:active,
|
||||
.datepicker table tr td.today.disabled:active,
|
||||
.datepicker table tr td.today.disabled:hover:active,
|
||||
.datepicker table tr td.today.active,
|
||||
.datepicker table tr td.today:hover.active,
|
||||
.datepicker table tr td.today.disabled.active,
|
||||
.datepicker table tr td.today.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.today,
|
||||
.open .dropdown-toggle.datepicker table tr td.today:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.today.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
|
||||
color: #000000;
|
||||
background-color: #ffcd70;
|
||||
border-color: #f59e00;
|
||||
}
|
||||
.datepicker table tr td.today:active,
|
||||
.datepicker table tr td.today:hover:active,
|
||||
.datepicker table tr td.today.disabled:active,
|
||||
.datepicker table tr td.today.disabled:hover:active,
|
||||
.datepicker table tr td.today.active,
|
||||
.datepicker table tr td.today:hover.active,
|
||||
.datepicker table tr td.today.disabled.active,
|
||||
.datepicker table tr td.today.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.today,
|
||||
.open .dropdown-toggle.datepicker table tr td.today:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.today.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.today.disabled:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.datepicker table tr td.today.disabled,
|
||||
.datepicker table tr td.today:hover.disabled,
|
||||
.datepicker table tr td.today.disabled.disabled,
|
||||
.datepicker table tr td.today.disabled:hover.disabled,
|
||||
.datepicker table tr td.today[disabled],
|
||||
.datepicker table tr td.today:hover[disabled],
|
||||
.datepicker table tr td.today.disabled[disabled],
|
||||
.datepicker table tr td.today.disabled:hover[disabled],
|
||||
fieldset[disabled] .datepicker table tr td.today,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
|
||||
.datepicker table tr td.today.disabled:hover,
|
||||
.datepicker table tr td.today:hover.disabled:hover,
|
||||
.datepicker table tr td.today.disabled.disabled:hover,
|
||||
.datepicker table tr td.today.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td.today[disabled]:hover,
|
||||
.datepicker table tr td.today:hover[disabled]:hover,
|
||||
.datepicker table tr td.today.disabled[disabled]:hover,
|
||||
.datepicker table tr td.today.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:hover,
|
||||
.datepicker table tr td.today.disabled:focus,
|
||||
.datepicker table tr td.today:hover.disabled:focus,
|
||||
.datepicker table tr td.today.disabled.disabled:focus,
|
||||
.datepicker table tr td.today.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td.today[disabled]:focus,
|
||||
.datepicker table tr td.today:hover[disabled]:focus,
|
||||
.datepicker table tr td.today.disabled[disabled]:focus,
|
||||
.datepicker table tr td.today.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:focus,
|
||||
.datepicker table tr td.today.disabled:active,
|
||||
.datepicker table tr td.today:hover.disabled:active,
|
||||
.datepicker table tr td.today.disabled.disabled:active,
|
||||
.datepicker table tr td.today.disabled:hover.disabled:active,
|
||||
.datepicker table tr td.today[disabled]:active,
|
||||
.datepicker table tr td.today:hover[disabled]:active,
|
||||
.datepicker table tr td.today.disabled[disabled]:active,
|
||||
.datepicker table tr td.today.disabled:hover[disabled]:active,
|
||||
fieldset[disabled] .datepicker table tr td.today:active,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover:active,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:active,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover:active,
|
||||
.datepicker table tr td.today.disabled.active,
|
||||
.datepicker table tr td.today:hover.disabled.active,
|
||||
.datepicker table tr td.today.disabled.disabled.active,
|
||||
.datepicker table tr td.today.disabled:hover.disabled.active,
|
||||
.datepicker table tr td.today[disabled].active,
|
||||
.datepicker table tr td.today:hover[disabled].active,
|
||||
.datepicker table tr td.today.disabled[disabled].active,
|
||||
.datepicker table tr td.today.disabled:hover[disabled].active,
|
||||
fieldset[disabled] .datepicker table tr td.today.active,
|
||||
fieldset[disabled] .datepicker table tr td.today:hover.active,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled.active,
|
||||
fieldset[disabled] .datepicker table tr td.today.disabled:hover.active {
|
||||
background-color: #ffdb99;
|
||||
border-color: #ffb733;
|
||||
}
|
||||
.datepicker table tr td.today:hover:hover {
|
||||
color: #000;
|
||||
}
|
||||
.datepicker table tr td.today.active:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.datepicker table tr td.range,
|
||||
.datepicker table tr td.range:hover,
|
||||
.datepicker table tr td.range.disabled,
|
||||
.datepicker table tr td.range.disabled:hover {
|
||||
background: #eeeeee;
|
||||
border-radius: 0;
|
||||
}
|
||||
.datepicker table tr td.range.today,
|
||||
.datepicker table tr td.range.today:hover,
|
||||
.datepicker table tr td.range.today.disabled,
|
||||
.datepicker table tr td.range.today.disabled:hover {
|
||||
color: #000000;
|
||||
background-color: #f7ca77;
|
||||
border-color: #f1a417;
|
||||
border-radius: 0;
|
||||
}
|
||||
.datepicker table tr td.range.today:hover,
|
||||
.datepicker table tr td.range.today:hover:hover,
|
||||
.datepicker table tr td.range.today.disabled:hover,
|
||||
.datepicker table tr td.range.today.disabled:hover:hover,
|
||||
.datepicker table tr td.range.today:focus,
|
||||
.datepicker table tr td.range.today:hover:focus,
|
||||
.datepicker table tr td.range.today.disabled:focus,
|
||||
.datepicker table tr td.range.today.disabled:hover:focus,
|
||||
.datepicker table tr td.range.today:active,
|
||||
.datepicker table tr td.range.today:hover:active,
|
||||
.datepicker table tr td.range.today.disabled:active,
|
||||
.datepicker table tr td.range.today.disabled:hover:active,
|
||||
.datepicker table tr td.range.today.active,
|
||||
.datepicker table tr td.range.today:hover.active,
|
||||
.datepicker table tr td.range.today.disabled.active,
|
||||
.datepicker table tr td.range.today.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
|
||||
color: #000000;
|
||||
background-color: #f4bb51;
|
||||
border-color: #bf800c;
|
||||
}
|
||||
.datepicker table tr td.range.today:active,
|
||||
.datepicker table tr td.range.today:hover:active,
|
||||
.datepicker table tr td.range.today.disabled:active,
|
||||
.datepicker table tr td.range.today.disabled:hover:active,
|
||||
.datepicker table tr td.range.today.active,
|
||||
.datepicker table tr td.range.today:hover.active,
|
||||
.datepicker table tr td.range.today.disabled.active,
|
||||
.datepicker table tr td.range.today.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.range.today.disabled:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.datepicker table tr td.range.today.disabled,
|
||||
.datepicker table tr td.range.today:hover.disabled,
|
||||
.datepicker table tr td.range.today.disabled.disabled,
|
||||
.datepicker table tr td.range.today.disabled:hover.disabled,
|
||||
.datepicker table tr td.range.today[disabled],
|
||||
.datepicker table tr td.range.today:hover[disabled],
|
||||
.datepicker table tr td.range.today.disabled[disabled],
|
||||
.datepicker table tr td.range.today.disabled:hover[disabled],
|
||||
fieldset[disabled] .datepicker table tr td.range.today,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
|
||||
.datepicker table tr td.range.today.disabled:hover,
|
||||
.datepicker table tr td.range.today:hover.disabled:hover,
|
||||
.datepicker table tr td.range.today.disabled.disabled:hover,
|
||||
.datepicker table tr td.range.today.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td.range.today[disabled]:hover,
|
||||
.datepicker table tr td.range.today:hover[disabled]:hover,
|
||||
.datepicker table tr td.range.today.disabled[disabled]:hover,
|
||||
.datepicker table tr td.range.today.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:hover,
|
||||
.datepicker table tr td.range.today.disabled:focus,
|
||||
.datepicker table tr td.range.today:hover.disabled:focus,
|
||||
.datepicker table tr td.range.today.disabled.disabled:focus,
|
||||
.datepicker table tr td.range.today.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td.range.today[disabled]:focus,
|
||||
.datepicker table tr td.range.today:hover[disabled]:focus,
|
||||
.datepicker table tr td.range.today.disabled[disabled]:focus,
|
||||
.datepicker table tr td.range.today.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:focus,
|
||||
.datepicker table tr td.range.today.disabled:active,
|
||||
.datepicker table tr td.range.today:hover.disabled:active,
|
||||
.datepicker table tr td.range.today.disabled.disabled:active,
|
||||
.datepicker table tr td.range.today.disabled:hover.disabled:active,
|
||||
.datepicker table tr td.range.today[disabled]:active,
|
||||
.datepicker table tr td.range.today:hover[disabled]:active,
|
||||
.datepicker table tr td.range.today.disabled[disabled]:active,
|
||||
.datepicker table tr td.range.today.disabled:hover[disabled]:active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover:active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover:active,
|
||||
.datepicker table tr td.range.today.disabled.active,
|
||||
.datepicker table tr td.range.today:hover.disabled.active,
|
||||
.datepicker table tr td.range.today.disabled.disabled.active,
|
||||
.datepicker table tr td.range.today.disabled:hover.disabled.active,
|
||||
.datepicker table tr td.range.today[disabled].active,
|
||||
.datepicker table tr td.range.today:hover[disabled].active,
|
||||
.datepicker table tr td.range.today.disabled[disabled].active,
|
||||
.datepicker table tr td.range.today.disabled:hover[disabled].active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today:hover.active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled.active,
|
||||
fieldset[disabled] .datepicker table tr td.range.today.disabled:hover.active {
|
||||
background-color: #f7ca77;
|
||||
border-color: #f1a417;
|
||||
}
|
||||
.datepicker table tr td.selected,
|
||||
.datepicker table tr td.selected:hover,
|
||||
.datepicker table tr td.selected.disabled,
|
||||
.datepicker table tr td.selected.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #999999;
|
||||
border-color: #555555;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td.selected:hover,
|
||||
.datepicker table tr td.selected:hover:hover,
|
||||
.datepicker table tr td.selected.disabled:hover,
|
||||
.datepicker table tr td.selected.disabled:hover:hover,
|
||||
.datepicker table tr td.selected:focus,
|
||||
.datepicker table tr td.selected:hover:focus,
|
||||
.datepicker table tr td.selected.disabled:focus,
|
||||
.datepicker table tr td.selected.disabled:hover:focus,
|
||||
.datepicker table tr td.selected:active,
|
||||
.datepicker table tr td.selected:hover:active,
|
||||
.datepicker table tr td.selected.disabled:active,
|
||||
.datepicker table tr td.selected.disabled:hover:active,
|
||||
.datepicker table tr td.selected.active,
|
||||
.datepicker table tr td.selected:hover.active,
|
||||
.datepicker table tr td.selected.disabled.active,
|
||||
.datepicker table tr td.selected.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #858585;
|
||||
border-color: #373737;
|
||||
}
|
||||
.datepicker table tr td.selected:active,
|
||||
.datepicker table tr td.selected:hover:active,
|
||||
.datepicker table tr td.selected.disabled:active,
|
||||
.datepicker table tr td.selected.disabled:hover:active,
|
||||
.datepicker table tr td.selected.active,
|
||||
.datepicker table tr td.selected:hover.active,
|
||||
.datepicker table tr td.selected.disabled.active,
|
||||
.datepicker table tr td.selected.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.selected.disabled:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.datepicker table tr td.selected.disabled,
|
||||
.datepicker table tr td.selected:hover.disabled,
|
||||
.datepicker table tr td.selected.disabled.disabled,
|
||||
.datepicker table tr td.selected.disabled:hover.disabled,
|
||||
.datepicker table tr td.selected[disabled],
|
||||
.datepicker table tr td.selected:hover[disabled],
|
||||
.datepicker table tr td.selected.disabled[disabled],
|
||||
.datepicker table tr td.selected.disabled:hover[disabled],
|
||||
fieldset[disabled] .datepicker table tr td.selected,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
|
||||
.datepicker table tr td.selected.disabled:hover,
|
||||
.datepicker table tr td.selected:hover.disabled:hover,
|
||||
.datepicker table tr td.selected.disabled.disabled:hover,
|
||||
.datepicker table tr td.selected.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td.selected[disabled]:hover,
|
||||
.datepicker table tr td.selected:hover[disabled]:hover,
|
||||
.datepicker table tr td.selected.disabled[disabled]:hover,
|
||||
.datepicker table tr td.selected.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:hover,
|
||||
.datepicker table tr td.selected.disabled:focus,
|
||||
.datepicker table tr td.selected:hover.disabled:focus,
|
||||
.datepicker table tr td.selected.disabled.disabled:focus,
|
||||
.datepicker table tr td.selected.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td.selected[disabled]:focus,
|
||||
.datepicker table tr td.selected:hover[disabled]:focus,
|
||||
.datepicker table tr td.selected.disabled[disabled]:focus,
|
||||
.datepicker table tr td.selected.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:focus,
|
||||
.datepicker table tr td.selected.disabled:active,
|
||||
.datepicker table tr td.selected:hover.disabled:active,
|
||||
.datepicker table tr td.selected.disabled.disabled:active,
|
||||
.datepicker table tr td.selected.disabled:hover.disabled:active,
|
||||
.datepicker table tr td.selected[disabled]:active,
|
||||
.datepicker table tr td.selected:hover[disabled]:active,
|
||||
.datepicker table tr td.selected.disabled[disabled]:active,
|
||||
.datepicker table tr td.selected.disabled:hover[disabled]:active,
|
||||
fieldset[disabled] .datepicker table tr td.selected:active,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover:active,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:active,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover:active,
|
||||
.datepicker table tr td.selected.disabled.active,
|
||||
.datepicker table tr td.selected:hover.disabled.active,
|
||||
.datepicker table tr td.selected.disabled.disabled.active,
|
||||
.datepicker table tr td.selected.disabled:hover.disabled.active,
|
||||
.datepicker table tr td.selected[disabled].active,
|
||||
.datepicker table tr td.selected:hover[disabled].active,
|
||||
.datepicker table tr td.selected.disabled[disabled].active,
|
||||
.datepicker table tr td.selected.disabled:hover[disabled].active,
|
||||
fieldset[disabled] .datepicker table tr td.selected.active,
|
||||
fieldset[disabled] .datepicker table tr td.selected:hover.active,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled.active,
|
||||
fieldset[disabled] .datepicker table tr td.selected.disabled:hover.active {
|
||||
background-color: #999999;
|
||||
border-color: #555555;
|
||||
}
|
||||
.datepicker table tr td.active,
|
||||
.datepicker table tr td.active:hover,
|
||||
.datepicker table tr td.active.disabled,
|
||||
.datepicker table tr td.active.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #428bca;
|
||||
border-color: #357ebd;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td.active:hover,
|
||||
.datepicker table tr td.active:hover:hover,
|
||||
.datepicker table tr td.active.disabled:hover,
|
||||
.datepicker table tr td.active.disabled:hover:hover,
|
||||
.datepicker table tr td.active:focus,
|
||||
.datepicker table tr td.active:hover:focus,
|
||||
.datepicker table tr td.active.disabled:focus,
|
||||
.datepicker table tr td.active.disabled:hover:focus,
|
||||
.datepicker table tr td.active:active,
|
||||
.datepicker table tr td.active:hover:active,
|
||||
.datepicker table tr td.active.disabled:active,
|
||||
.datepicker table tr td.active.disabled:hover:active,
|
||||
.datepicker table tr td.active.active,
|
||||
.datepicker table tr td.active:hover.active,
|
||||
.datepicker table tr td.active.disabled.active,
|
||||
.datepicker table tr td.active.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.active:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.active.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #3276b1;
|
||||
border-color: #285e8e;
|
||||
}
|
||||
.datepicker table tr td.active:active,
|
||||
.datepicker table tr td.active:hover:active,
|
||||
.datepicker table tr td.active.disabled:active,
|
||||
.datepicker table tr td.active.disabled:hover:active,
|
||||
.datepicker table tr td.active.active,
|
||||
.datepicker table tr td.active:hover.active,
|
||||
.datepicker table tr td.active.disabled.active,
|
||||
.datepicker table tr td.active.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.active,
|
||||
.open .dropdown-toggle.datepicker table tr td.active:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td.active.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td.active.disabled:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.datepicker table tr td.active.disabled,
|
||||
.datepicker table tr td.active:hover.disabled,
|
||||
.datepicker table tr td.active.disabled.disabled,
|
||||
.datepicker table tr td.active.disabled:hover.disabled,
|
||||
.datepicker table tr td.active[disabled],
|
||||
.datepicker table tr td.active:hover[disabled],
|
||||
.datepicker table tr td.active.disabled[disabled],
|
||||
.datepicker table tr td.active.disabled:hover[disabled],
|
||||
fieldset[disabled] .datepicker table tr td.active,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
|
||||
.datepicker table tr td.active.disabled:hover,
|
||||
.datepicker table tr td.active:hover.disabled:hover,
|
||||
.datepicker table tr td.active.disabled.disabled:hover,
|
||||
.datepicker table tr td.active.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td.active[disabled]:hover,
|
||||
.datepicker table tr td.active:hover[disabled]:hover,
|
||||
.datepicker table tr td.active.disabled[disabled]:hover,
|
||||
.datepicker table tr td.active.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:hover,
|
||||
.datepicker table tr td.active.disabled:focus,
|
||||
.datepicker table tr td.active:hover.disabled:focus,
|
||||
.datepicker table tr td.active.disabled.disabled:focus,
|
||||
.datepicker table tr td.active.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td.active[disabled]:focus,
|
||||
.datepicker table tr td.active:hover[disabled]:focus,
|
||||
.datepicker table tr td.active.disabled[disabled]:focus,
|
||||
.datepicker table tr td.active.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:focus,
|
||||
.datepicker table tr td.active.disabled:active,
|
||||
.datepicker table tr td.active:hover.disabled:active,
|
||||
.datepicker table tr td.active.disabled.disabled:active,
|
||||
.datepicker table tr td.active.disabled:hover.disabled:active,
|
||||
.datepicker table tr td.active[disabled]:active,
|
||||
.datepicker table tr td.active:hover[disabled]:active,
|
||||
.datepicker table tr td.active.disabled[disabled]:active,
|
||||
.datepicker table tr td.active.disabled:hover[disabled]:active,
|
||||
fieldset[disabled] .datepicker table tr td.active:active,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover:active,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:active,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover:active,
|
||||
.datepicker table tr td.active.disabled.active,
|
||||
.datepicker table tr td.active:hover.disabled.active,
|
||||
.datepicker table tr td.active.disabled.disabled.active,
|
||||
.datepicker table tr td.active.disabled:hover.disabled.active,
|
||||
.datepicker table tr td.active[disabled].active,
|
||||
.datepicker table tr td.active:hover[disabled].active,
|
||||
.datepicker table tr td.active.disabled[disabled].active,
|
||||
.datepicker table tr td.active.disabled:hover[disabled].active,
|
||||
fieldset[disabled] .datepicker table tr td.active.active,
|
||||
fieldset[disabled] .datepicker table tr td.active:hover.active,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled.active,
|
||||
fieldset[disabled] .datepicker table tr td.active.disabled:hover.active {
|
||||
background-color: #428bca;
|
||||
border-color: #357ebd;
|
||||
}
|
||||
.datepicker table tr td span {
|
||||
display: block;
|
||||
width: 23%;
|
||||
height: 54px;
|
||||
line-height: 54px;
|
||||
float: left;
|
||||
margin: 1%;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.datepicker table tr td span:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.datepicker table tr td span.disabled,
|
||||
.datepicker table tr td span.disabled:hover {
|
||||
background: none;
|
||||
color: #999999;
|
||||
cursor: default;
|
||||
}
|
||||
.datepicker table tr td span.active,
|
||||
.datepicker table tr td span.active:hover,
|
||||
.datepicker table tr td span.active.disabled,
|
||||
.datepicker table tr td span.active.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #428bca;
|
||||
border-color: #357ebd;
|
||||
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.datepicker table tr td span.active:hover,
|
||||
.datepicker table tr td span.active:hover:hover,
|
||||
.datepicker table tr td span.active.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled:hover:hover,
|
||||
.datepicker table tr td span.active:focus,
|
||||
.datepicker table tr td span.active:hover:focus,
|
||||
.datepicker table tr td span.active.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled:hover:focus,
|
||||
.datepicker table tr td span.active:active,
|
||||
.datepicker table tr td span.active:hover:active,
|
||||
.datepicker table tr td span.active.disabled:active,
|
||||
.datepicker table tr td span.active.disabled:hover:active,
|
||||
.datepicker table tr td span.active.active,
|
||||
.datepicker table tr td span.active:hover.active,
|
||||
.datepicker table tr td span.active.disabled.active,
|
||||
.datepicker table tr td span.active.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
|
||||
color: #ffffff;
|
||||
background-color: #3276b1;
|
||||
border-color: #285e8e;
|
||||
}
|
||||
.datepicker table tr td span.active:active,
|
||||
.datepicker table tr td span.active:hover:active,
|
||||
.datepicker table tr td span.active.disabled:active,
|
||||
.datepicker table tr td span.active.disabled:hover:active,
|
||||
.datepicker table tr td span.active.active,
|
||||
.datepicker table tr td span.active:hover.active,
|
||||
.datepicker table tr td span.active.disabled.active,
|
||||
.datepicker table tr td span.active.disabled:hover.active,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active:hover,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled,
|
||||
.open .dropdown-toggle.datepicker table tr td span.active.disabled:hover {
|
||||
background-image: none;
|
||||
}
|
||||
.datepicker table tr td span.active.disabled,
|
||||
.datepicker table tr td span.active:hover.disabled,
|
||||
.datepicker table tr td span.active.disabled.disabled,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled,
|
||||
.datepicker table tr td span.active[disabled],
|
||||
.datepicker table tr td span.active:hover[disabled],
|
||||
.datepicker table tr td span.active.disabled[disabled],
|
||||
.datepicker table tr td span.active.disabled:hover[disabled],
|
||||
fieldset[disabled] .datepicker table tr td span.active,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled:hover,
|
||||
.datepicker table tr td span.active:hover.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled.disabled:hover,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled:hover,
|
||||
.datepicker table tr td span.active[disabled]:hover,
|
||||
.datepicker table tr td span.active:hover[disabled]:hover,
|
||||
.datepicker table tr td span.active.disabled[disabled]:hover,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled]:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:hover,
|
||||
.datepicker table tr td span.active.disabled:focus,
|
||||
.datepicker table tr td span.active:hover.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled.disabled:focus,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled:focus,
|
||||
.datepicker table tr td span.active[disabled]:focus,
|
||||
.datepicker table tr td span.active:hover[disabled]:focus,
|
||||
.datepicker table tr td span.active.disabled[disabled]:focus,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled]:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:focus,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:focus,
|
||||
.datepicker table tr td span.active.disabled:active,
|
||||
.datepicker table tr td span.active:hover.disabled:active,
|
||||
.datepicker table tr td span.active.disabled.disabled:active,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled:active,
|
||||
.datepicker table tr td span.active[disabled]:active,
|
||||
.datepicker table tr td span.active:hover[disabled]:active,
|
||||
.datepicker table tr td span.active.disabled[disabled]:active,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled]:active,
|
||||
fieldset[disabled] .datepicker table tr td span.active:active,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover:active,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:active,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover:active,
|
||||
.datepicker table tr td span.active.disabled.active,
|
||||
.datepicker table tr td span.active:hover.disabled.active,
|
||||
.datepicker table tr td span.active.disabled.disabled.active,
|
||||
.datepicker table tr td span.active.disabled:hover.disabled.active,
|
||||
.datepicker table tr td span.active[disabled].active,
|
||||
.datepicker table tr td span.active:hover[disabled].active,
|
||||
.datepicker table tr td span.active.disabled[disabled].active,
|
||||
.datepicker table tr td span.active.disabled:hover[disabled].active,
|
||||
fieldset[disabled] .datepicker table tr td span.active.active,
|
||||
fieldset[disabled] .datepicker table tr td span.active:hover.active,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled.active,
|
||||
fieldset[disabled] .datepicker table tr td span.active.disabled:hover.active {
|
||||
background-color: #428bca;
|
||||
border-color: #357ebd;
|
||||
}
|
||||
.datepicker table tr td span.old,
|
||||
.datepicker table tr td span.new {
|
||||
color: #999999;
|
||||
}
|
||||
.datepicker th.datepicker-switch {
|
||||
width: 145px;
|
||||
}
|
||||
.datepicker thead tr:first-child th,
|
||||
.datepicker tfoot tr th {
|
||||
cursor: pointer;
|
||||
}
|
||||
.datepicker thead tr:first-child th:hover,
|
||||
.datepicker tfoot tr th:hover {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.datepicker .cw {
|
||||
font-size: 10px;
|
||||
width: 12px;
|
||||
padding: 0 2px 0 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.datepicker thead tr:first-child th.cw {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
.input-group.date .input-group-addon i {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.input-daterange input {
|
||||
text-align: center;
|
||||
}
|
||||
.input-daterange input:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.input-daterange input:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.input-daterange .input-group-addon {
|
||||
width: auto;
|
||||
min-width: 16px;
|
||||
padding: 4px 5px;
|
||||
font-weight: normal;
|
||||
line-height: 1.428571429;
|
||||
text-align: center;
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
vertical-align: middle;
|
||||
background-color: #eeeeee;
|
||||
border-width: 1px 0;
|
||||
margin-left: -5px;
|
||||
margin-right: -5px;
|
||||
}
|
||||
.datepicker.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
float: left;
|
||||
display: none;
|
||||
min-width: 160px;
|
||||
list-style: none;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
|
||||
-webkit-background-clip: padding-box;
|
||||
-moz-background-clip: padding;
|
||||
background-clip: padding-box;
|
||||
*border-right-width: 2px;
|
||||
*border-bottom-width: 2px;
|
||||
color: #333333;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.428571429;
|
||||
}
|
||||
.datepicker.dropdown-menu th,
|
||||
.datepicker.dropdown-menu td {
|
||||
padding: 4px 5px;
|
||||
}
|
|
@ -1,155 +0,0 @@
|
|||
/* The MIT License */
|
||||
.dropzone,
|
||||
.dropzone *,
|
||||
.dropzone-previews,
|
||||
.dropzone-previews * {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dropzone {
|
||||
position: relative;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
background: rgba(0,0,0,0.02);
|
||||
padding: 1em;
|
||||
}
|
||||
.dropzone.dz-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropzone.dz-clickable .dz-message,
|
||||
.dropzone.dz-clickable .dz-message span {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropzone.dz-clickable * {
|
||||
cursor: default;
|
||||
}
|
||||
.dropzone .dz-message {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.dropzone.dz-drag-hover {
|
||||
border-color: rgba(0,0,0,0.15);
|
||||
background: rgba(0,0,0,0.04);
|
||||
}
|
||||
.dropzone.dz-started .dz-message {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview,
|
||||
.dropzone-previews .dz-preview {
|
||||
background: rgba(255,255,255,0.8);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 17px;
|
||||
vertical-align: top;
|
||||
border: 1px solid #acacac;
|
||||
padding: 6px 6px 6px 6px;
|
||||
}
|
||||
.dropzone .dz-preview.dz-file-preview [data-dz-thumbnail],
|
||||
.dropzone-previews .dz-preview.dz-file-preview [data-dz-thumbnail] {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details,
|
||||
.dropzone-previews .dz-preview .dz-details {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
background: #ebebeb;
|
||||
padding: 5px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details .dz-filename,
|
||||
.dropzone-previews .dz-preview .dz-details .dz-filename {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details img,
|
||||
.dropzone-previews .dz-preview .dz-details img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details .dz-size,
|
||||
.dropzone-previews .dz-preview .dz-details .dz-size {
|
||||
position: absolute;
|
||||
bottom: -28px;
|
||||
left: 3px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.dropzone .dz-preview.dz-error .dz-error-mark,
|
||||
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview.dz-success .dz-success-mark,
|
||||
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview:hover .dz-details img,
|
||||
.dropzone-previews .dz-preview:hover .dz-details img {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark,
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
right: -10px;
|
||||
top: -10px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark {
|
||||
color: #8cc657;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark {
|
||||
color: #ee162d;
|
||||
}
|
||||
.dropzone .dz-preview .dz-progress,
|
||||
.dropzone-previews .dz-preview .dz-progress {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
height: 6px;
|
||||
background: #d7d7d7;
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-progress .dz-upload,
|
||||
.dropzone-previews .dz-preview .dz-progress .dz-upload {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
background-color: #8cc657;
|
||||
}
|
||||
.dropzone .dz-preview.dz-processing .dz-progress,
|
||||
.dropzone-previews .dz-preview.dz-processing .dz-progress {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-message,
|
||||
.dropzone-previews .dz-preview .dz-error-message {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -20px;
|
||||
background: rgba(245,245,245,0.8);
|
||||
padding: 8px 10px;
|
||||
color: #800;
|
||||
min-width: 140px;
|
||||
max-width: 500px;
|
||||
z-index: 500;
|
||||
}
|
||||
.dropzone .dz-preview:hover.dz-error .dz-error-message,
|
||||
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
|
||||
display: block;
|
||||
}
|
|
@ -1,410 +0,0 @@
|
|||
/* The MIT License */
|
||||
.dropzone,
|
||||
.dropzone *,
|
||||
.dropzone-previews,
|
||||
.dropzone-previews * {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dropzone {
|
||||
position: relative;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
background: rgba(0,0,0,0.02);
|
||||
padding: 1em;
|
||||
}
|
||||
.dropzone.dz-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropzone.dz-clickable .dz-message,
|
||||
.dropzone.dz-clickable .dz-message span {
|
||||
cursor: pointer;
|
||||
}
|
||||
.dropzone.dz-clickable * {
|
||||
cursor: default;
|
||||
}
|
||||
.dropzone .dz-message {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.dropzone.dz-drag-hover {
|
||||
border-color: rgba(0,0,0,0.15);
|
||||
background: rgba(0,0,0,0.04);
|
||||
}
|
||||
.dropzone.dz-started .dz-message {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview,
|
||||
.dropzone-previews .dz-preview {
|
||||
background: rgba(255,255,255,0.8);
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 17px;
|
||||
vertical-align: top;
|
||||
border: 1px solid #acacac;
|
||||
padding: 6px 6px 6px 6px;
|
||||
}
|
||||
.dropzone .dz-preview.dz-file-preview [data-dz-thumbnail],
|
||||
.dropzone-previews .dz-preview.dz-file-preview [data-dz-thumbnail] {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details,
|
||||
.dropzone-previews .dz-preview .dz-details {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
position: relative;
|
||||
background: #ebebeb;
|
||||
padding: 5px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details .dz-filename,
|
||||
.dropzone-previews .dz-preview .dz-details .dz-filename {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details img,
|
||||
.dropzone-previews .dz-preview .dz-details img {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-details .dz-size,
|
||||
.dropzone-previews .dz-preview .dz-details .dz-size {
|
||||
position: absolute;
|
||||
bottom: -28px;
|
||||
left: 3px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
}
|
||||
.dropzone .dz-preview.dz-error .dz-error-mark,
|
||||
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview.dz-success .dz-success-mark,
|
||||
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview:hover .dz-details img,
|
||||
.dropzone-previews .dz-preview:hover .dz-details img {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark,
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 30px;
|
||||
text-align: center;
|
||||
right: -10px;
|
||||
top: -10px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark {
|
||||
color: #8cc657;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark {
|
||||
color: #ee162d;
|
||||
}
|
||||
.dropzone .dz-preview .dz-progress,
|
||||
.dropzone-previews .dz-preview .dz-progress {
|
||||
position: absolute;
|
||||
top: 100px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
height: 6px;
|
||||
background: #d7d7d7;
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-progress .dz-upload,
|
||||
.dropzone-previews .dz-preview .dz-progress .dz-upload {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
background-color: #8cc657;
|
||||
}
|
||||
.dropzone .dz-preview.dz-processing .dz-progress,
|
||||
.dropzone-previews .dz-preview.dz-processing .dz-progress {
|
||||
display: block;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-message,
|
||||
.dropzone-previews .dz-preview .dz-error-message {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: -20px;
|
||||
background: rgba(245,245,245,0.8);
|
||||
padding: 8px 10px;
|
||||
color: #800;
|
||||
min-width: 140px;
|
||||
max-width: 500px;
|
||||
z-index: 500;
|
||||
}
|
||||
.dropzone .dz-preview:hover.dz-error .dz-error-message,
|
||||
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
|
||||
display: block;
|
||||
}
|
||||
.dropzone {
|
||||
border: 1px solid rgba(0,0,0,0.03);
|
||||
min-height: 360px;
|
||||
-webkit-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0,0,0,0.03);
|
||||
padding: 23px;
|
||||
}
|
||||
.dropzone .dz-default.dz-message {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
-webkit-transition: opacity 0.3s ease-in-out;
|
||||
-moz-transition: opacity 0.3s ease-in-out;
|
||||
-o-transition: opacity 0.3s ease-in-out;
|
||||
-ms-transition: opacity 0.3s ease-in-out;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
background-image: url("../images/spritemap.png");
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
position: absolute;
|
||||
width: 428px;
|
||||
height: 123px;
|
||||
margin-left: -214px;
|
||||
margin-top: -61.5px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
|
||||
.dropzone .dz-default.dz-message {
|
||||
background-image: url("../images/spritemap@2x.png");
|
||||
-webkit-background-size: 428px 406px;
|
||||
-moz-background-size: 428px 406px;
|
||||
background-size: 428px 406px;
|
||||
}
|
||||
}
|
||||
.dropzone .dz-default.dz-message span {
|
||||
display: none;
|
||||
}
|
||||
.dropzone.dz-square .dz-default.dz-message {
|
||||
background-position: 0 -123px;
|
||||
width: 268px;
|
||||
margin-left: -134px;
|
||||
height: 174px;
|
||||
margin-top: -87px;
|
||||
}
|
||||
.dropzone.dz-drag-hover .dz-message {
|
||||
opacity: 0.15;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=15)";
|
||||
filter: alpha(opacity=15);
|
||||
}
|
||||
.dropzone.dz-started .dz-message {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
filter: alpha(opacity=0);
|
||||
}
|
||||
.dropzone .dz-preview,
|
||||
.dropzone-previews .dz-preview {
|
||||
-webkit-box-shadow: 1px 1px 4px rgba(0,0,0,0.16);
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,0.16);
|
||||
font-size: 14px;
|
||||
}
|
||||
.dropzone .dz-preview.dz-image-preview:hover .dz-details img,
|
||||
.dropzone-previews .dz-preview.dz-image-preview:hover .dz-details img {
|
||||
display: block;
|
||||
opacity: 0.1;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=10)";
|
||||
filter: alpha(opacity=10);
|
||||
}
|
||||
.dropzone .dz-preview.dz-success .dz-success-mark,
|
||||
.dropzone-previews .dz-preview.dz-success .dz-success-mark {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.dropzone .dz-preview.dz-error .dz-error-mark,
|
||||
.dropzone-previews .dz-preview.dz-error .dz-error-mark {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.dropzone .dz-preview.dz-error .dz-progress .dz-upload,
|
||||
.dropzone-previews .dz-preview.dz-error .dz-progress .dz-upload {
|
||||
background: #ee1e2d;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark,
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
filter: alpha(opacity=0);
|
||||
-webkit-transition: opacity 0.4s ease-in-out;
|
||||
-moz-transition: opacity 0.4s ease-in-out;
|
||||
-o-transition: opacity 0.4s ease-in-out;
|
||||
-ms-transition: opacity 0.4s ease-in-out;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
background-image: url("../images/spritemap.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark,
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark {
|
||||
background-image: url("../images/spritemap@2x.png");
|
||||
-webkit-background-size: 428px 406px;
|
||||
-moz-background-size: 428px 406px;
|
||||
background-size: 428px 406px;
|
||||
}
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-mark span,
|
||||
.dropzone-previews .dz-preview .dz-error-mark span,
|
||||
.dropzone .dz-preview .dz-success-mark span,
|
||||
.dropzone-previews .dz-preview .dz-success-mark span {
|
||||
display: none;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-mark,
|
||||
.dropzone-previews .dz-preview .dz-error-mark {
|
||||
background-position: -268px -123px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-success-mark,
|
||||
.dropzone-previews .dz-preview .dz-success-mark {
|
||||
background-position: -268px -163px;
|
||||
}
|
||||
.dropzone .dz-preview .dz-progress .dz-upload,
|
||||
.dropzone-previews .dz-preview .dz-progress .dz-upload {
|
||||
-webkit-animation: loading 0.4s linear infinite;
|
||||
-moz-animation: loading 0.4s linear infinite;
|
||||
-o-animation: loading 0.4s linear infinite;
|
||||
-ms-animation: loading 0.4s linear infinite;
|
||||
animation: loading 0.4s linear infinite;
|
||||
-webkit-transition: width 0.3s ease-in-out;
|
||||
-moz-transition: width 0.3s ease-in-out;
|
||||
-o-transition: width 0.3s ease-in-out;
|
||||
-ms-transition: width 0.3s ease-in-out;
|
||||
transition: width 0.3s ease-in-out;
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-image: url("../images/spritemap.png");
|
||||
background-repeat: repeat-x;
|
||||
background-position: 0px -400px;
|
||||
}
|
||||
@media all and (-webkit-min-device-pixel-ratio:1.5),(min--moz-device-pixel-ratio:1.5),(-o-min-device-pixel-ratio:1.5/1),(min-device-pixel-ratio:1.5),(min-resolution:138dpi),(min-resolution:1.5dppx) {
|
||||
.dropzone .dz-preview .dz-progress .dz-upload,
|
||||
.dropzone-previews .dz-preview .dz-progress .dz-upload {
|
||||
background-image: url("../images/spritemap@2x.png");
|
||||
-webkit-background-size: 428px 406px;
|
||||
-moz-background-size: 428px 406px;
|
||||
background-size: 428px 406px;
|
||||
}
|
||||
}
|
||||
.dropzone .dz-preview.dz-success .dz-progress,
|
||||
.dropzone-previews .dz-preview.dz-success .dz-progress {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
filter: alpha(opacity=0);
|
||||
-webkit-transition: opacity 0.4s ease-in-out;
|
||||
-moz-transition: opacity 0.4s ease-in-out;
|
||||
-o-transition: opacity 0.4s ease-in-out;
|
||||
-ms-transition: opacity 0.4s ease-in-out;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
.dropzone .dz-preview .dz-error-message,
|
||||
.dropzone-previews .dz-preview .dz-error-message {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
|
||||
filter: alpha(opacity=0);
|
||||
-webkit-transition: opacity 0.3s ease-in-out;
|
||||
-moz-transition: opacity 0.3s ease-in-out;
|
||||
-o-transition: opacity 0.3s ease-in-out;
|
||||
-ms-transition: opacity 0.3s ease-in-out;
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
.dropzone .dz-preview:hover.dz-error .dz-error-message,
|
||||
.dropzone-previews .dz-preview:hover.dz-error .dz-error-message {
|
||||
opacity: 1;
|
||||
-ms-filter: none;
|
||||
filter: none;
|
||||
}
|
||||
.dropzone a.dz-remove,
|
||||
.dropzone-previews a.dz-remove {
|
||||
background-image: -webkit-linear-gradient(top, #fafafa, #eee);
|
||||
background-image: -moz-linear-gradient(top, #fafafa, #eee);
|
||||
background-image: -o-linear-gradient(top, #fafafa, #eee);
|
||||
background-image: -ms-linear-gradient(top, #fafafa, #eee);
|
||||
background-image: linear-gradient(to bottom, #fafafa, #eee);
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #eee;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
padding: 4px 5px;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
margin-top: 26px;
|
||||
}
|
||||
.dropzone a.dz-remove:hover,
|
||||
.dropzone-previews a.dz-remove:hover {
|
||||
color: #666;
|
||||
}
|
||||
@-moz-keyframes loading {
|
||||
0% {
|
||||
background-position: 0 -400px;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -7px -400px;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes loading {
|
||||
0% {
|
||||
background-position: 0 -400px;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -7px -400px;
|
||||
}
|
||||
}
|
||||
@-o-keyframes loading {
|
||||
0% {
|
||||
background-position: 0 -400px;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -7px -400px;
|
||||
}
|
||||
}
|
||||
@-ms-keyframes loading {
|
||||
0% {
|
||||
background-position: 0 -400px;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -7px -400px;
|
||||
}
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
background-position: 0 -400px;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: -7px -400px;
|
||||
}
|
||||
}
|
|
@ -1,977 +0,0 @@
|
|||
/*!
|
||||
* FullCalendar v2.2.0 Stylesheet
|
||||
* Docs & License: http://arshaw.com/fullcalendar/
|
||||
* (c) 2013 Adam Shaw
|
||||
*/
|
||||
|
||||
|
||||
.fc {
|
||||
direction: ltr;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fc-rtl {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body .fc { /* extra precedence to overcome jqui */
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Colors
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-unthemed th,
|
||||
.fc-unthemed td,
|
||||
.fc-unthemed hr,
|
||||
.fc-unthemed thead,
|
||||
.fc-unthemed tbody,
|
||||
.fc-unthemed .fc-row,
|
||||
.fc-unthemed .fc-popover {
|
||||
border-color: #ddd;
|
||||
}
|
||||
|
||||
.fc-unthemed .fc-popover {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.fc-unthemed hr,
|
||||
.fc-unthemed .fc-popover .fc-header {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.fc-unthemed .fc-popover .fc-header .fc-close {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.fc-unthemed .fc-today {
|
||||
background: #fcf8e3;
|
||||
}
|
||||
|
||||
.fc-highlight { /* when user is selecting cells */
|
||||
background: #bce8f1;
|
||||
opacity: .3;
|
||||
filter: alpha(opacity=30); /* for IE */
|
||||
}
|
||||
|
||||
.fc-bgevent { /* default look for background events */
|
||||
background: rgb(143, 223, 130);
|
||||
opacity: .3;
|
||||
filter: alpha(opacity=30); /* for IE */
|
||||
}
|
||||
|
||||
.fc-nonbusiness { /* default look for non-business-hours areas */
|
||||
/* will inherit .fc-bgevent's styles */
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
|
||||
/* Icons (inline elements with styled text that mock arrow icons)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-icon {
|
||||
display: inline-block;
|
||||
font-size: 2em;
|
||||
line-height: .5em;
|
||||
height: .5em; /* will make the total height 1em */
|
||||
font-family: "Courier New", Courier, monospace;
|
||||
}
|
||||
|
||||
.fc-icon-left-single-arrow:after {
|
||||
content: "\02039";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fc-icon-right-single-arrow:after {
|
||||
content: "\0203A";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.fc-icon-left-double-arrow:after {
|
||||
content: "\000AB";
|
||||
}
|
||||
|
||||
.fc-icon-right-double-arrow:after {
|
||||
content: "\000BB";
|
||||
}
|
||||
|
||||
.fc-icon-x:after {
|
||||
content: "\000D7";
|
||||
}
|
||||
|
||||
|
||||
/* Buttons (styled <button> tags, normalized to work cross-browser)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc button {
|
||||
/* force height to include the border and padding */
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* dimensions */
|
||||
margin: 0;
|
||||
height: 2.1em;
|
||||
padding: 0 .6em;
|
||||
|
||||
/* text & cursor */
|
||||
font-size: 1em; /* normalize */
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Firefox has an annoying inner border */
|
||||
.fc button::-moz-focus-inner { margin: 0; padding: 0; }
|
||||
|
||||
.fc-state-default { /* non-theme */
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.fc-state-default.fc-corner-left { /* non-theme */
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
|
||||
.fc-state-default.fc-corner-right { /* non-theme */
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
/* icons in buttons */
|
||||
|
||||
.fc button .fc-icon { /* non-theme */
|
||||
position: relative;
|
||||
top: .05em; /* seems to be a good adjustment across browsers */
|
||||
margin: 0 .1em;
|
||||
}
|
||||
|
||||
/*
|
||||
button states
|
||||
borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
|
||||
*/
|
||||
|
||||
.fc-state-default {
|
||||
background-color: #f5f5f5;
|
||||
background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
|
||||
background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
|
||||
background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
|
||||
background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
|
||||
background-repeat: repeat-x;
|
||||
border-color: #e6e6e6 #e6e6e6 #bfbfbf;
|
||||
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
|
||||
color: #333;
|
||||
text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.fc-state-hover,
|
||||
.fc-state-down,
|
||||
.fc-state-active,
|
||||
.fc-state-disabled {
|
||||
color: #333333;
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.fc-state-hover {
|
||||
color: #333333;
|
||||
text-decoration: none;
|
||||
background-position: 0 -15px;
|
||||
-webkit-transition: background-position 0.1s linear;
|
||||
-moz-transition: background-position 0.1s linear;
|
||||
-o-transition: background-position 0.1s linear;
|
||||
transition: background-position 0.1s linear;
|
||||
}
|
||||
|
||||
.fc-state-down,
|
||||
.fc-state-active {
|
||||
background-color: #cccccc;
|
||||
background-image: none;
|
||||
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.fc-state-disabled {
|
||||
cursor: default;
|
||||
background-image: none;
|
||||
opacity: 0.65;
|
||||
filter: alpha(opacity=65);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
/* Buttons Groups
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-button-group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*
|
||||
every button that is not first in a button group should scootch over one pixel and cover the
|
||||
previous button's border...
|
||||
*/
|
||||
|
||||
.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */
|
||||
float: left;
|
||||
margin: 0 0 0 -1px;
|
||||
}
|
||||
|
||||
.fc .fc-button-group > :first-child { /* same */
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Popover
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-popover {
|
||||
position: absolute;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,.15);
|
||||
}
|
||||
|
||||
.fc-popover .fc-header {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
.fc-popover .fc-header .fc-title {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.fc-popover .fc-header .fc-close {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-popover .fc-header .fc-title,
|
||||
.fc-rtl .fc-popover .fc-header .fc-close {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-popover .fc-header .fc-title,
|
||||
.fc-ltr .fc-popover .fc-header .fc-close {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* unthemed */
|
||||
|
||||
.fc-unthemed .fc-popover {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.fc-unthemed .fc-popover .fc-header .fc-close {
|
||||
font-size: 25px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* jqui themed */
|
||||
|
||||
.fc-popover > .ui-widget-header + .ui-widget-content {
|
||||
border-top: 0; /* where they meet, let the header have the border */
|
||||
}
|
||||
|
||||
|
||||
/* Misc Reusable Components
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc hr {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0 0 2px; /* height is unreliable across browsers, so use padding */
|
||||
border-style: solid;
|
||||
border-width: 1px 0;
|
||||
}
|
||||
|
||||
.fc-clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.fc-bg,
|
||||
.fc-bgevent-skeleton,
|
||||
.fc-highlight-skeleton,
|
||||
.fc-helper-skeleton {
|
||||
/* these element should always cling to top-left/right corners */
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fc-bg {
|
||||
bottom: 0; /* strech bg to bottom edge */
|
||||
}
|
||||
|
||||
.fc-bg table {
|
||||
height: 100%; /* strech bg to bottom edge */
|
||||
}
|
||||
|
||||
|
||||
/* Tables
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
font-size: 1em; /* normalize cross-browser */
|
||||
}
|
||||
|
||||
.fc th {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fc th,
|
||||
.fc td {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.fc td.fc-today {
|
||||
border-style: double; /* overcome neighboring borders */
|
||||
}
|
||||
|
||||
|
||||
/* Fake Table Rows
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */
|
||||
/* no visible border by default. but make available if need be (scrollbar width compensation) */
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.fc-row table {
|
||||
/* don't put left/right border on anything within a fake row.
|
||||
the outer tbody will worry about this */
|
||||
border-left: 0 hidden transparent;
|
||||
border-right: 0 hidden transparent;
|
||||
|
||||
/* no bottom borders on rows */
|
||||
border-bottom: 0 hidden transparent;
|
||||
}
|
||||
|
||||
.fc-row:first-child table {
|
||||
border-top: 0 hidden transparent; /* no top border on first row */
|
||||
}
|
||||
|
||||
|
||||
/* Day Row (used within the header and the DayGrid)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fc-row .fc-bg {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* highlighting cells & background event skeleton */
|
||||
|
||||
.fc-row .fc-bgevent-skeleton,
|
||||
.fc-row .fc-highlight-skeleton {
|
||||
bottom: 0; /* stretch skeleton to bottom of row */
|
||||
}
|
||||
|
||||
.fc-row .fc-bgevent-skeleton table,
|
||||
.fc-row .fc-highlight-skeleton table {
|
||||
height: 100%; /* stretch skeleton to bottom of row */
|
||||
}
|
||||
|
||||
.fc-row .fc-highlight-skeleton td,
|
||||
.fc-row .fc-bgevent-skeleton td {
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.fc-row .fc-bgevent-skeleton {
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.fc-row .fc-highlight-skeleton {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
/*
|
||||
row content (which contains day/week numbers and events) as well as "helper" (which contains
|
||||
temporary rendered events).
|
||||
*/
|
||||
|
||||
.fc-row .fc-content-skeleton {
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
padding-bottom: 2px; /* matches the space above the events */
|
||||
}
|
||||
|
||||
.fc-row .fc-helper-skeleton {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.fc-row .fc-content-skeleton td,
|
||||
.fc-row .fc-helper-skeleton td {
|
||||
/* see-through to the background below */
|
||||
background: none; /* in case <td>s are globally styled */
|
||||
border-color: transparent;
|
||||
|
||||
/* don't put a border between events and/or the day number */
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */
|
||||
.fc-row .fc-helper-skeleton tbody td {
|
||||
/* don't put a border between event cells */
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Scrolling Container
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-scroller { /* this class goes on elements for guaranteed vertical scrollbars */
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.fc-scroller > * { /* we expect an immediate inner element */
|
||||
position: relative; /* re-scope all positions */
|
||||
width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */
|
||||
overflow: hidden; /* don't let negative margins or absolute positioning create further scroll */
|
||||
}
|
||||
|
||||
|
||||
/* Global Event Styles
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-event {
|
||||
position: relative; /* for resize handle and other inner positioning */
|
||||
display: block; /* make the <a> tag block */
|
||||
font-size: .85em;
|
||||
line-height: 1.3;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #3a87ad; /* default BORDER color */
|
||||
background-color: #3a87ad; /* default BACKGROUND color */
|
||||
font-weight: normal; /* undo jqui's ui-widget-header bold */
|
||||
}
|
||||
|
||||
/* overpower some of bootstrap's and jqui's styles on <a> tags */
|
||||
.fc-event,
|
||||
.fc-event:hover,
|
||||
.ui-widget .fc-event {
|
||||
color: #fff; /* default TEXT color */
|
||||
text-decoration: none; /* if <a> has an href */
|
||||
}
|
||||
|
||||
.fc-event[href],
|
||||
.fc-event.fc-draggable {
|
||||
cursor: pointer; /* give events with links and draggable events a hand mouse pointer */
|
||||
}
|
||||
|
||||
.fc-not-allowed, /* causes a "warning" cursor. applied on body */
|
||||
.fc-not-allowed .fc-event { /* to override an event's custom cursor */
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
/* DayGrid events
|
||||
----------------------------------------------------------------------------------------------------
|
||||
We use the full "fc-day-grid-event" class instead of using descendants because the event won't
|
||||
be a descendant of the grid when it is being dragged.
|
||||
*/
|
||||
|
||||
.fc-day-grid-event {
|
||||
margin: 1px 2px 0; /* spacing between events and edges */
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */
|
||||
|
||||
.fc-ltr .fc-day-grid-event.fc-not-start,
|
||||
.fc-rtl .fc-day-grid-event.fc-not-end {
|
||||
margin-left: 0;
|
||||
border-left-width: 0;
|
||||
padding-left: 1px; /* replace the border with padding */
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-day-grid-event.fc-not-end,
|
||||
.fc-rtl .fc-day-grid-event.fc-not-start {
|
||||
margin-right: 0;
|
||||
border-right-width: 0;
|
||||
padding-right: 1px; /* replace the border with padding */
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.fc-day-grid-event > .fc-content { /* force events to be one-line tall */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fc-day-grid-event .fc-time {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* resize handle (outside of fc-content, so can go outside of bounds) */
|
||||
|
||||
.fc-day-grid-event .fc-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-day-grid-event .fc-resizer {
|
||||
right: -3px;
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-day-grid-event .fc-resizer {
|
||||
left: -3px;
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
|
||||
/* Event Limiting
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* "more" link that represents hidden events */
|
||||
|
||||
a.fc-more {
|
||||
margin: 1px 3px;
|
||||
font-size: .85em;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.fc-more:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.fc-limited { /* rows and cells that are hidden because of a "more" link */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* popover that appears when "more" link is clicked */
|
||||
|
||||
.fc-day-grid .fc-row {
|
||||
z-index: 1; /* make the "more" popover one higher than this */
|
||||
}
|
||||
|
||||
.fc-more-popover {
|
||||
z-index: 2;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-event-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* Toolbar
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-toolbar {
|
||||
text-align: center;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-center {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* the things within each left/right/center section */
|
||||
.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */
|
||||
float: left;
|
||||
margin-left: .75em;
|
||||
}
|
||||
|
||||
/* the first thing within each left/center/right section */
|
||||
.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* title text */
|
||||
|
||||
.fc-toolbar h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* button layering (for border precedence) */
|
||||
|
||||
.fc-toolbar button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-state-hover,
|
||||
.fc-toolbar .ui-state-hover {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-state-down {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.fc-toolbar .fc-state-active,
|
||||
.fc-toolbar .ui-state-active {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.fc-toolbar button:focus {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
|
||||
/* View Structure
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */
|
||||
/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */
|
||||
.fc-view-container *,
|
||||
.fc-view-container *:before,
|
||||
.fc-view-container *:after {
|
||||
-webkit-box-sizing: content-box;
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.fc-view, /* scope positioning and z-index's for everything within the view */
|
||||
.fc-view > table { /* so dragged elements can be above the view's main element */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* BasicView
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* day row structure */
|
||||
|
||||
.fc-basicWeek-view .fc-content-skeleton,
|
||||
.fc-basicDay-view .fc-content-skeleton {
|
||||
/* we are sure there are no day numbers in these views, so... */
|
||||
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
|
||||
padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */
|
||||
}
|
||||
|
||||
.fc-basic-view tbody .fc-row {
|
||||
min-height: 4em; /* ensure that all rows are at least this tall */
|
||||
}
|
||||
|
||||
/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */
|
||||
|
||||
.fc-row.fc-rigid {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fc-row.fc-rigid .fc-content-skeleton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* week and day number styling */
|
||||
|
||||
.fc-basic-view .fc-week-number,
|
||||
.fc-basic-view .fc-day-number {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.fc-basic-view td.fc-week-number span,
|
||||
.fc-basic-view td.fc-day-number {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.fc-basic-view .fc-week-number {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fc-basic-view .fc-week-number span {
|
||||
/* work around the way we do column resizing and ensure a minimum width */
|
||||
display: inline-block;
|
||||
min-width: 1.25em;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-basic-view .fc-day-number {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-basic-view .fc-day-number {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fc-day-number.fc-other-month {
|
||||
opacity: 0.3;
|
||||
filter: alpha(opacity=30); /* for IE */
|
||||
/* opacity with small font can sometimes look too faded
|
||||
might want to set the 'color' property instead
|
||||
making day-numbers bold also fixes the problem */
|
||||
}
|
||||
|
||||
/* AgendaView all-day area
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-agenda-view .fc-day-grid {
|
||||
position: relative;
|
||||
z-index: 2; /* so the "more.." popover will be over the time grid */
|
||||
}
|
||||
|
||||
.fc-agenda-view .fc-day-grid .fc-row {
|
||||
min-height: 3em; /* all-day section will never get shorter than this */
|
||||
}
|
||||
|
||||
.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton {
|
||||
padding-top: 1px; /* add a pixel to make sure there are 2px padding above events */
|
||||
padding-bottom: 1em; /* give space underneath events for clicking/selecting days */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid axis running down the side (for both the all-day area and the slot area)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc .fc-axis { /* .fc to overcome default cell styles */
|
||||
vertical-align: middle;
|
||||
padding: 0 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-axis {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-axis {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-widget td.fc-axis {
|
||||
font-weight: normal; /* overcome jqui theme making it bold */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Structure
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-time-grid-container, /* so scroll container's z-index is below all-day */
|
||||
.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fc-time-grid {
|
||||
min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */
|
||||
}
|
||||
|
||||
.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */
|
||||
border: 0 hidden transparent;
|
||||
}
|
||||
|
||||
.fc-time-grid > .fc-bg {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-slats,
|
||||
.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-bgevent-skeleton,
|
||||
.fc-time-grid .fc-content-skeleton {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-bgevent-skeleton {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-highlight-skeleton {
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-content-skeleton {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-helper-skeleton {
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Slats (lines that run horizontally)
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-slats td {
|
||||
height: 1.5em;
|
||||
border-bottom: 0; /* each cell is responsible for its top border */
|
||||
}
|
||||
|
||||
.fc-slats .fc-minor td {
|
||||
border-top-style: dotted;
|
||||
}
|
||||
|
||||
.fc-slats .ui-widget-content { /* for jqui theme */
|
||||
background: none; /* see through to fc-bg */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Highlighting Slots
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */
|
||||
position: relative; /* scopes the left/right of the fc-highlight to be in the column */
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-highlight {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
/* top and bottom will be in by JS */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Event Containment
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-time-grid .fc-event-container, /* a div within a cell within the fc-content-skeleton */
|
||||
.fc-time-grid .fc-bgevent-container { /* a div within a cell within the fc-bgevent-skeleton */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */
|
||||
margin: 0 2.5% 0 2px;
|
||||
}
|
||||
|
||||
.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */
|
||||
margin: 0 2px 0 2.5%;
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-event,
|
||||
.fc-time-grid .fc-bgevent {
|
||||
position: absolute;
|
||||
z-index: 1; /* scope inner z-index's */
|
||||
}
|
||||
|
||||
.fc-time-grid .fc-bgevent {
|
||||
/* background events always span full width */
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Event Styling
|
||||
----------------------------------------------------------------------------------------------------
|
||||
We use the full "fc-time-grid-event" class instead of using descendants because the event won't
|
||||
be a descendant of the grid when it is being dragged.
|
||||
*/
|
||||
|
||||
.fc-time-grid-event.fc-not-start { /* events that are continuing from another day */
|
||||
/* replace space made by the top border with padding */
|
||||
border-top-width: 0;
|
||||
padding-top: 1px;
|
||||
|
||||
/* remove top rounded corners */
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-not-end {
|
||||
/* replace space made by the top border with padding */
|
||||
border-bottom-width: 0;
|
||||
padding-bottom: 1px;
|
||||
|
||||
/* remove bottom rounded corners */
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.fc-time-grid-event {
|
||||
overflow: hidden; /* don't let the bg flow over rounded corners */
|
||||
}
|
||||
|
||||
.fc-time-grid-event > .fc-content { /* contains the time and title, but no bg and resizer */
|
||||
position: relative;
|
||||
z-index: 2; /* above the bg */
|
||||
}
|
||||
|
||||
.fc-time-grid-event .fc-time,
|
||||
.fc-time-grid-event .fc-title {
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.fc-time-grid-event .fc-time {
|
||||
font-size: .85em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fc-time-grid-event .fc-bg {
|
||||
z-index: 1;
|
||||
background: #fff;
|
||||
opacity: .25;
|
||||
filter: alpha(opacity=25); /* for IE */
|
||||
}
|
||||
|
||||
/* short mode, where time and title are on the same line */
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-content {
|
||||
/* don't wrap to second line (now that contents will be inline) */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-time,
|
||||
.fc-time-grid-event.fc-short .fc-title {
|
||||
/* put the time and title on the same line */
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-time span {
|
||||
display: none; /* don't display the full time text... */
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-time:before {
|
||||
content: attr(data-start); /* ...instead, display only the start time */
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-time:after {
|
||||
content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */
|
||||
}
|
||||
|
||||
.fc-time-grid-event.fc-short .fc-title {
|
||||
font-size: .85em; /* make the title text the same size as the time */
|
||||
padding: 0; /* undo padding from above */
|
||||
}
|
||||
|
||||
/* resizer */
|
||||
|
||||
.fc-time-grid-event .fc-resizer {
|
||||
position: absolute;
|
||||
z-index: 3; /* above content */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 8px;
|
||||
overflow: hidden;
|
||||
line-height: 8px;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
text-align: center;
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.fc-time-grid-event .fc-resizer:after {
|
||||
content: "=";
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
/*!
|
||||
* FullCalendar v2.2.0 Print Stylesheet
|
||||
* Docs & License: http://arshaw.com/fullcalendar/
|
||||
* (c) 2013 Adam Shaw
|
||||
*/
|
||||
|
||||
/*
|
||||
* Include this stylesheet on your page to get a more printer-friendly calendar.
|
||||
* When including this stylesheet, use the media='print' attribute of the <link> tag.
|
||||
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
|
||||
*/
|
||||
|
||||
.fc {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
|
||||
/* Global Event Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-event {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.fc-event .fc-resizer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* Table & Day-Row Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
th,
|
||||
td,
|
||||
hr,
|
||||
thead,
|
||||
tbody,
|
||||
.fc-row {
|
||||
border-color: #ccc !important;
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
/* kill the overlaid, absolutely-positioned common components */
|
||||
.fc-bg,
|
||||
.fc-bgevent-skeleton,
|
||||
.fc-highlight-skeleton,
|
||||
.fc-helper-skeleton {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* don't force a min-height on rows (for DayGrid) */
|
||||
.fc tbody .fc-row {
|
||||
height: auto !important; /* undo height that JS set in distributeHeight */
|
||||
min-height: 0 !important; /* undo the min-height from each view's specific stylesheet */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton {
|
||||
position: static; /* undo .fc-rigid */
|
||||
padding-bottom: 0 !important; /* use a more border-friendly method for this... */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td { /* only works in newer browsers */
|
||||
padding-bottom: 1em; /* ...gives space within the skeleton. also ensures min height in a way */
|
||||
}
|
||||
|
||||
.fc tbody .fc-row .fc-content-skeleton table {
|
||||
/* provides a min-height for the row, but only effective for IE, which exaggerates this value,
|
||||
making it look more like 3em. for other browers, it will already be this tall */
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
|
||||
/* Undo month-view event limiting. Display all events and hide the "more" links
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-more-cell,
|
||||
.fc-more {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fc tr.fc-limited {
|
||||
display: table-row !important;
|
||||
}
|
||||
|
||||
.fc td.fc-limited {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
display: none; /* never display the "more.." popover in print mode */
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* undo the min-height 100% trick used to fill the container's height */
|
||||
.fc-time-grid {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
/* don't display the side axis at all ("all-day" and time cells) */
|
||||
.fc-agenda-view .fc-axis {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* don't display the horizontal lines */
|
||||
.fc-slats,
|
||||
.fc-time-grid hr { /* this hr is used when height is underused and needs to be filled */
|
||||
display: none !important; /* important overrides inline declaration */
|
||||
}
|
||||
|
||||
/* let the container that holds the events be naturally positioned and create real height */
|
||||
.fc-time-grid .fc-content-skeleton {
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* in case there are no events, we still want some height */
|
||||
.fc-time-grid .fc-content-skeleton table {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
/* kill the horizontal spacing made by the event container. event margins will be done below */
|
||||
.fc-time-grid .fc-event-container {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* TimeGrid *Event* Restyling
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* naturally position events, vertically stacking them */
|
||||
.fc-time-grid .fc-event {
|
||||
position: static !important;
|
||||
margin: 3px 2px !important;
|
||||
}
|
||||
|
||||
/* for events that continue to a future day, give the bottom border back */
|
||||
.fc-time-grid .fc-event.fc-not-end {
|
||||
border-bottom-width: 1px !important;
|
||||
}
|
||||
|
||||
/* indicate the event continues via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-end:after {
|
||||
content: "...";
|
||||
}
|
||||
|
||||
/* for events that are continuations from previous days, give the top border back */
|
||||
.fc-time-grid .fc-event.fc-not-start {
|
||||
border-top-width: 1px !important;
|
||||
}
|
||||
|
||||
/* indicate the event is a continuation via "..." text */
|
||||
.fc-time-grid .fc-event.fc-not-start:before {
|
||||
content: "...";
|
||||
}
|
||||
|
||||
/* time */
|
||||
|
||||
/* undo a previous declaration and let the time text span to a second line */
|
||||
.fc-time-grid .fc-event .fc-time {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
/* hide the the time that is normally displayed... */
|
||||
.fc-time-grid .fc-event .fc-time span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ...replace it with a more verbose version (includes AM/PM) stored in an html attribute */
|
||||
.fc-time-grid .fc-event .fc-time:after {
|
||||
content: attr(data-full);
|
||||
}
|
||||
|
||||
|
||||
/* Vertical Scroller & Containers
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* kill the scrollbars and allow natural height */
|
||||
.fc-scroller,
|
||||
.fc-day-grid-container, /* these divs might be assigned height, which we need to cleared */
|
||||
.fc-time-grid-container { /* */
|
||||
overflow: visible !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
/* kill the horizontal border/padding used to compensate for scrollbars */
|
||||
.fc-row {
|
||||
border: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
/* Button Controls
|
||||
--------------------------------------------------------------------------------------------------*/
|
||||
|
||||
.fc-button-group,
|
||||
.fc button {
|
||||
display: none; /* don't display any button-related controls */
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/* iCheck plugin Square skin, green
|
||||
----------------------------------- */
|
||||
.icheckbox_square-green,
|
||||
.iradio_square-green {
|
||||
display: inline-block;
|
||||
*display: inline;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background: url(green.png) no-repeat;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icheckbox_square-green {
|
||||
background-position: 0 0;
|
||||
}
|
||||
.icheckbox_square-green.hover {
|
||||
background-position: -24px 0;
|
||||
}
|
||||
.icheckbox_square-green.checked {
|
||||
background-position: -48px 0;
|
||||
}
|
||||
.icheckbox_square-green.disabled {
|
||||
background-position: -72px 0;
|
||||
cursor: default;
|
||||
}
|
||||
.icheckbox_square-green.checked.disabled {
|
||||
background-position: -96px 0;
|
||||
}
|
||||
|
||||
.iradio_square-green {
|
||||
background-position: -120px 0;
|
||||
}
|
||||
.iradio_square-green.hover {
|
||||
background-position: -144px 0;
|
||||
}
|
||||
.iradio_square-green.checked {
|
||||
background-position: -168px 0;
|
||||
}
|
||||
.iradio_square-green.disabled {
|
||||
background-position: -192px 0;
|
||||
cursor: default;
|
||||
}
|
||||
.iradio_square-green.checked.disabled {
|
||||
background-position: -216px 0;
|
||||
}
|
||||
|
||||
/* HiDPI support */
|
||||
@media (-o-min-device-pixel-ratio: 5/4), (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) {
|
||||
.icheckbox_square-green,
|
||||
.iradio_square-green {
|
||||
background-image: url(green@2x.png);
|
||||
-webkit-background-size: 240px 24px;
|
||||
background-size: 240px 24px;
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 7.5 KiB |