commit 66906a14241dcb483368cd0ba9ecf91feafc28a6 Author: leebaok Date: Thu Mar 31 16:03:38 2016 +0800 [Init] init repository with Docklet 0.2.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fec9b46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ +*.pyc +*.swp +__temp +*~ +.DS_Store +docklet.conf diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3393b29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2016, Peking University (PKU). +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the PKU nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE.INCLUDING NEGLIGENCE OR OTHERWISE diff --git a/README.md b/README.md new file mode 100644 index 0000000..847c333 --- /dev/null +++ b/README.md @@ -0,0 +1,199 @@ +# Docklet + +## intro + +Docklet is an operating system for mini-datacener. Its goal is to help +multi-user share cluster resources effectively. Unlike the "application +framework oriented" cluster manager such as Mesos and Yarn, Docklet is +**user oriented**. In Docklet, every user has their own private +**virtual cluster (vcluster)**, which consists of a number of virtual +Linux container nodes distributed over the physical cluster. Every +vcluster is separated from others and can be operated like a real +physical cluster. Therefore, most applications, especially those +requiring a cluster environment, can run in vcluster seamlessly. + +Docklet provides a base image for creating virtual nodes. This image has +pre-installed a lot of mainstream development tools and frameworks, +including gcc/g++, openjdk, python3, R, MPI, scala, ruby, php, node.js, +texlive, mpich2, spark, +scipy/numpy/matplotlib/pandas/sympy/scikit-learn, jupyter notebook, etc. +Users can get a ready vcluster with just one click within 1 second. + +The users are free to install their specific software in their vcluster. +Docklet supports operating through **web terminal**. Users can do their +work as an administrator working on a console. The base image system is +ubuntu. The recommended way of installing new software is by +**apt-get**. + +The users manage and use their vcluster all through web. The only client +tool needed is a modern web browser, like safari, firefox, chrome. The +integrated *jupyter notebook* provides a web workspace. By visiting the +workspace, users can do coding, debugging and testing of their programs +online. The **python scipy** series of tools can even display graphical +pictures in the browser. Therefore, it is ideal for data analysis and +processing. + +Docklet only need **one** public IP address. The vclusters are +configured to use private IP address range, e.g., 172.16.0.0/16, +192.168.0.0/16, 10.0.0.0/8. A proxy is setup to help +users visit their vclusters behind the firewall/gateway. + +The Docklet system runtime consists of four components: + +- distributed file system server +- etcd server +- docklet master +- docklet worker + +## install + +Currently the docklet runtime is recommend to run in Unbuntu 15.10+. + +Ensure that python3.5 is the default python3 version. + +Unpack the docklet tarball to a directory ( /root/docklet as an +example), will get + +``` +readme.md +prepare.sh +conf/ + container.conf + docklet.conf.template + lxc-script/ +bin/ + docklet-master + docklet-worker +src/ + httprest.py + worker.py + ... +web/ + web.py +dep/ + etcd-multi-nodes.sh + etcd-one-node.sh +doc/ +tools/ + update-basefs.sh + start_jupyter.sh +``` + +If it is the first time install, users should run **prepare.sh** to +install necessary packages automatically. Note it may need to run this +script several times to successfully install all the needed packages. + +A *root* users will be created for managing the system. The password is +recorded in `FS_PREFIX/local/generated_password.txt` . + +## config ## + +The main configuration file of docklet is conf/docklet.conf. Most +default setting works for a single host environment. + +First copy docklet.conf.template to get docklet.conf. + +The following settings should be taken care of: + +- NETWORK_DEVICE : the network device to use. +- ETCD : the etcd server address. For distributed muli hosts + environment, it should be one of the ETCD public server address. + For single host environment, the default value should be OK. +- STORAGE : using disk or file to storage persistent data, for + single host, file is convenient. +- FS_PREFIX: the working dir of docklet runtime. default is + /opt/docklet. +- CLUSTER_NET: the vcluster network ip address range, default is + 172.16.0.1/16. This network range should all be allocated to and + managed by docklet. +- PROXY_PORT : the public port of docklet. Users use + this port to visit the docklet system. +- PORTAL_URL : the portal of the system. Users access the system + by visiting this address. If the system is behind a firewall, then + a reverse proxy should be setup. + +## start ## + +### distributed file system ### + +For multi hosts distributed environment, a distributed file system is +needed to store global data. Currently, glusterfs has been tested. +Lets presume the file system server export filesystem as nfs +**fileserver:/pub** : + +In each physical host to run docklet, mount **fileserver:/pub** to +**FS_PEFIX/global** . + +For single host environment, it need not to configure distributed +file system. + +### etcd ### + +For single host environment, start **dep/etcd-one-node.sh** . Some recent +Ubuntu releases have included **etcd** in the repository, just `apt-get +install etcd`, and it need not to start etcd manually. + +For multi hosts distributed environment, start +**dep/etcd-multi-nodes.sh** in each etcd server hosts. This scripts +requires users providing the etcd server address as parameters. + +### master ### + +First, select a server with 2 network interface card, one having a +public IP address/url, e.g., docklet.info; the other having a private IP +address, e.g., 172.16.0.1. This server will be the master. + +If it is the first time you start docklet, run `bin/docklet-master init` +to init and start docklet master. Otherwise, run `bin/docklet-master start`, +which will start master in recovery mode in background using +conf/docklet.conf. It means docklet will recover workspaces existed. + +This script in fact will start three daemons: the docklet master of +httprest.py, the configurable-http-proxy and the docklet web of web.py. + +You can check the daemon status by running `bin/docklet-master status` + +If the master failed to start, you could try `bin/docklet-master init` +to initialize the whole system. + +More usages can be found by typing `bin/docklet-master` + +The master logs are in **FS_PREFIX/local/log/docklet-master.log** and +**docklet-web.log**. + +### worker ### + +Worker needs a basefs image to boot container. + +You can create such an image with `lxc-create -n test -t download`, +and then copy the rootfs to **FS_PREFIX/local**, and renamed `rootfs` +to `basefs`. + +Note the `jupyerhub` package must be installed for this image. And the +start script `tools/start_jupyter.sh` should be placed at +`basefs/home/jupyter`. + +You can check and run `tools/update-basefs.sh` to update basefs. + +Run `bin/docklet-worker start`, will start worker in background. + +You can check the daemon status by running `bin/docklet-worker status` + +More usages can be found by typing `bin/docklet-worker` + +The log is in **FS_PREFIX/local/log/docklet-worker.log** + +Currently, the worker must be run after the master has been started. + +## usage ## + +Open a browser, visiting the address specified by PORTAL_URL , +e.g., ` http://docklet.info/ ` + +If the system is just deployed in single host for testing purpose, +then the PORTAL_URL defaults to `http://MASTER_IP:PROXY_PORT`, +e.g., `http://localhost:8000`. + +That is it. + +## system admin ## diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..53a75d6 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.2.6 diff --git a/bin/docklet-master b/bin/docklet-master new file mode 100755 index 0000000..0d7c489 --- /dev/null +++ b/bin/docklet-master @@ -0,0 +1,230 @@ +#!/bin/sh + +[ $(id -u) != '0' ] && echo "root is needed" && exit 1 + +# get some path of docklet + +bindir=${0%/*} +# $bindir maybe like /opt/docklet/src/../sbin +# use command below to make $bindir in normal absolute path +DOCKLET_BIN=$(cd $bindir; pwd) +DOCKLET_HOME=${DOCKLET_BIN%/*} +DOCKLET_CONF=$DOCKLET_HOME/conf +LXC_SCRIPT=$DOCKLET_CONF/lxc-script +DOCKLET_SRC=$DOCKLET_HOME/src +DOCKLET_LIB=$DOCKLET_SRC +DOCKLET_WEB=$DOCKLET_HOME/web + +# default working directory, default to /opt/docklet +FS_PREFIX=/opt/docklet + +RUN_DIR=$FS_PREFIX/local/run +LOG_DIR=$FS_PREFIX/local/log + +#configurable-http-proxy public port, default is 8000 +PROXY_PORT=8000 +#configurable-http-proxy api port, default is 8001 +PROXY_API_PORT=8001 +#network interface , default is eth0 +NETWORK_DEVICE=eth0 +#etcd server address, default is localhost:2379 +ETCD=localhost:2379 +#unique cluster_name, default is docklet-vc +CLUSTER_NAME=docklet-vc +#web port, default is 8888 +WEB_PORT=8888 +#cluster net, default is 172.16.0.1/16 +CLUSTER_NET="172.16.0.1/16" + +. $DOCKLET_CONF/docklet.conf + +export FS_PREFIX + +# This next line determines what user the script runs as. +DAEMON_USER=root + +# settings for docklet master +DAEMON_MASTER=$DOCKLET_LIB/httprest.py +DAEMON_NAME_MASTER=docklet-master +DAEMON_OPTS_MASTER= +# The process ID of the script when it runs is stored here: +PIDFILE_MASTER=$RUN_DIR/$DAEMON_NAME_MASTER.pid + +# settings for docklet proxy, which is required for web access +DAEMON_PROXY=`which configurable-http-proxy` +DAEMON_NAME_PROXY=docklet-proxy +PIDFILE_PROXY=$RUN_DIR/proxy.pid +DAEMON_OPTS_PROXY= + +# settings for docklet web +DAEMON_WEB=$DOCKLET_WEB/web.py +DAEMON_NAME_WEB=docklet-web +PIDFILE_WEB=$RUN_DIR/docklet-web.pid +DAEMON_OPTS_WEB= + +RUNNING_CONFIG=$FS_PREFIX/local/docklet-running.conf +export CONFIG=$RUNNING_CONFIG + +. /lib/lsb/init-functions + +########### + +pre_start_master () { + log_daemon_msg "Starting $DAEMON_NAME_MASTER in $FS_PREFIX" + + [ ! -d $FS_PREFIX/global ] && mkdir -p $FS_PREFIX/global + [ ! -d $FS_PREFIX/local ] && mkdir -p $FS_PREFIX/local + [ ! -d $FS_PREFIX/global/users ] && mkdir -p $FS_PREFIX/global/users + [ ! -d $FS_PREFIX/local/volume ] && mkdir -p $FS_PREFIX/local/volume + [ ! -d $FS_PREFIX/local/temp ] && mkdir -p $FS_PREFIX/local/temp + [ ! -d $FS_PREFIX/local/run ] && mkdir -p $FS_PREFIX/local/run + [ ! -d $FS_PREFIX/local/log ] && mkdir -p $FS_PREFIX/local/log + + grep -P "^[\s]*[a-zA-Z]" $DOCKLET_CONF/docklet.conf > $RUNNING_CONFIG + + echo "DOCKLET_HOME=$DOCKLET_HOME" >> $RUNNING_CONFIG + echo "DOCKLET_BIN=$DOCKLET_BIN" >> $RUNNING_CONFIG + echo "DOCKLET_CONF=$DOCKLET_CONF" >> $RUNNING_CONFIG + echo "LXC_SCRIPT=$LXC_SCRIPT" >> $RUNNING_CONFIG + echo "DOCKLET_SRC=$DOCKLET_SRC" >> $RUNNING_CONFIG + echo "DOCKLET_LIB=$DOCKLET_LIB" >> $RUNNING_CONFIG + + + # iptables for NAT network for containers to access web + iptables -t nat -F + iptables -t nat -A POSTROUTING -s $CLUSTER_NET -j MASQUERADE + +} + +do_start_master () { + pre_start_master + + DAEMON_OPTS_MASTER=$1 + + # MODE : start mode + # new : clean old data in etcd, global directory and start a new cluster + # recovery : start cluster and recover status from etcd and global directory + # Default is "recovery" + + start-stop-daemon --start --oknodo --background --pidfile $PIDFILE_MASTER --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_MASTER -- $DAEMON_OPTS_MASTER + log_end_msg $? +} + + +do_start_proxy () { + log_daemon_msg "Starting $DAEMON_NAME_PROXY daemon in $FS_PREFIX" + DAEMON_OPTS_PROXY="--port $PROXY_PORT --api-port $PROXY_API_PORT --default-target=http://localhost:8888" + start-stop-daemon --start --background --pidfile $PIDFILE_PROXY --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_PROXY -- $DAEMON_OPTS_PROXY + log_end_msg $? +} + +pre_start_web () { + log_daemon_msg "Starting $DAEMON_NAME_WEB in $FS_PREFIX" + + webip=$(ip addr show $NETWORK_DEVICE | grep -oE "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+") + + [ $? != "0" ] && echo "wrong NETWORK_DEVICE $NETWORK_DEVICE" && exit 1 + + webip=${webip%/*} + + AUTH_COOKIE_URL=http://$webip:$WEB_PORT/jupyter + #echo "set AUTH_COOKIE_URL:$AUTH_COOKIE_URL in etcd with key:$CLUSTER_NAME/web/authurl" + curl -XPUT http://$ETCD/v2/keys/$CLUSTER_NAME/web/authurl -d value="$AUTH_COOKIE_URL" > /dev/null 2>&1 + [ $? != 0 ] && echo "set AUTH_COOKIE_URL failed in etcd" && exit 1 +} + +do_start_web () { + pre_start_web + + DAEMON_OPTS_WEB="-p $WEB_PORT" + + start-stop-daemon --start --background --pidfile $PIDFILE_WEB --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_WEB -- $DAEMON_OPTS_WEB + log_end_msg $? +} + + +do_stop_master () { + log_daemon_msg "Stopping $DAEMON_NAME_MASTER daemon" + start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_MASTER --retry 10 + log_end_msg $? +} + +do_stop_proxy () { + log_daemon_msg "Stopping $DAEMON_NAME_PROXY daemon" + start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_PROXY --retry 10 + log_end_msg $? +} + + +do_stop_web () { + log_daemon_msg "Stopping $DAEMON_NAME_WEB daemon" + start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_WEB --retry 10 + log_end_msg $? +} + +case "$1" in + init) + do_start_master "new" + do_start_proxy + do_start_web + ;; + start) + do_start_master "recovery" + do_start_proxy + do_start_web + ;; + + stop) + do_stop_web + do_stop_proxy + do_stop_master + ;; + + restart) + do_stop_web + do_stop_proxy + do_stop_master + do_start_master "recovery" + do_start_proxy + do_start_web + ;; + + start_proxy) + do_start_proxy + ;; + + stop_proxy) + do_stop_proxy + ;; + + start_web) + do_start_web + ;; + + stop_web) + do_stop_web + ;; + + reinit) + do_stop_web + do_stop_proxy + do_stop_master + do_start_master "new" + do_start_proxy + do_start_web + ;; + + status) + status=0 + status_of_proc -p $PIDFILE_MASTER "$DAEMON_MASTER" "$DAEMON_NAME_MASTER" || status=$? + status_of_proc -p $PIDFILE_PROXY "$DAEMON_PROXY" "$DAEMON_NAME_PROXY" || status=$? + status_of_proc -p $PIDFILE_WEB "$DAEMON_WEB" "$DAEMON_NAME_WEB" || status=$? + exit $status + ;; + + *) + echo "Usage: $DAEMON_NAME_MASTER {init|start|stop|restart|reinit|status|start_proxy|stop_proxy|start_web|stop_web}" + exit 1 + ;; +esac +exit 0 diff --git a/bin/docklet-worker b/bin/docklet-worker new file mode 100755 index 0000000..6e95a84 --- /dev/null +++ b/bin/docklet-worker @@ -0,0 +1,124 @@ +#!/bin/sh + +[ $(id -u) != '0' ] && echo "root is needed" && exit 1 + +# get some path of docklet + +bindir=${0%/*} +# $bindir maybe like /opt/docklet/src/../bin +# use command below to make $bindir in normal absolute path +DOCKLET_BIN=$(cd $bindir; pwd) +DOCKLET_HOME=${DOCKLET_BIN%/*} +DOCKLET_CONF=$DOCKLET_HOME/conf +LXC_SCRIPT=$DOCKLET_CONF/lxc-script +DOCKLET_SRC=$DOCKLET_HOME/src +DOCKLET_LIB=$DOCKLET_SRC +DOCKLET_WEB=$DOCKLET_HOME/web + +# working directory, default to /opt/docklet +FS_PREFIX=/opt/docklet + +# cluster net ip range, default is 172.16.0.1/16 +CLUSTER_NET="172.16.0.1/16" + +RUN_DIR=$FS_PREFIX/local/run +LOG_DIR=$FS_PREFIX/local/log + +. $DOCKLET_CONF/docklet.conf + +export FS_PREFIX + +# This next line determines what user the script runs as. +DAEMON_USER=root + +# settings for docklet worker +DAEMON=$DOCKLET_LIB/worker.py +DAEMON_NAME=docklet-worker +DAEMON_OPTS= +# The process ID of the script when it runs is stored here: +PIDFILE=$RUN_DIR/$DAEMON_NAME.pid + +. /lib/lsb/init-functions + +########### + +pre_start () { + log_daemon_msg "Starting $DAEMON_NAME in $FS_PREFIX" + + [ ! -d $FS_PREFIX/global ] && mkdir -p $FS_PREFIX/global + [ ! -d $FS_PREFIX/local ] && mkdir -p $FS_PREFIX/local + [ ! -d $FS_PREFIX/global/users ] && mkdir -p $FS_PREFIX/global/users + [ ! -d $FS_PREFIX/local/volume ] && mkdir -p $FS_PREFIX/local/volume + [ ! -d $FS_PREFIX/local/temp ] && mkdir -p $FS_PREFIX/local/temp + [ ! -d $FS_PREFIX/local/run ] && mkdir -p $FS_PREFIX/local/run + [ ! -d $FS_PREFIX/local/log ] && mkdir -p $FS_PREFIX/local/log + + tempdir=/opt/docklet/local/temp + + RUNNING_CONFIG=$FS_PREFIX/local/docklet-running.conf + + grep -P "^[\s]*[a-zA-Z]" $DOCKLET_CONF/docklet.conf > $RUNNING_CONFIG + + echo "DOCKLET_HOME=$DOCKLET_HOME" >> $RUNNING_CONFIG + echo "DOCKLET_BIN=$DOCKLET_BIN" >> $RUNNING_CONFIG + echo "DOCKLET_CONF=$DOCKLET_CONF" >> $RUNNING_CONFIG + echo "LXC_SCRIPT=$LXC_SCRIPT" >> $RUNNING_CONFIG + echo "DOCKLET_SRC=$DOCKLET_SRC" >> $RUNNING_CONFIG + echo "DOCKLET_LIB=$DOCKLET_LIB" >> $RUNNING_CONFIG + + export CONFIG=$RUNNING_CONFIG + + # iptables for NAT network for containers to access web + iptables -t nat -F + iptables -t nat -A POSTROUTING -s $CLUSTER_NET -j MASQUERADE + + if [ ! -d $FS_PREFIX/local/basefs ]; then + log_daemon_msg "create basefs ..." + [ ! -f $tempdir/basefs.tar.bz ] && log_daemon_msg "$tempdir/basefs.tar.bz not exist, run prepare.sh first" && exit 1 + tar xvf $tempdir/basefs.tar.bz -C $FS_PREFIX/local > /dev/null + fi +} + +do_start() { + pre_start + start-stop-daemon --start --oknodo --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS + log_end_msg $? +} + +do_stop () { + log_daemon_msg "Stopping $DAEMON_NAME daemon" + start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE --retry 10 + log_end_msg $? +} + + + +case "$1" in + start) + do_start + ;; + + stop) + do_stop + ;; + + console) + pre_start + cprofilev $DAEMON $DAEMON_OPTS + ;; + + restart) + do_stop + do_start + ;; + + status) + status_of_proc -p $PIDFILE "$DAEMON" "$DAEMON_NAME" && exit 0 || exit $? + ;; + + *) + echo "Usage: $DAEMON_NAME {start|stop|restart|status}" + exit 1 + ;; +esac +exit 0 diff --git a/conf/container.conf b/conf/container.conf new file mode 100644 index 0000000..1445173 --- /dev/null +++ b/conf/container.conf @@ -0,0 +1,51 @@ +# This is the common container.conf for all containers. +# If want set custom settings, you have two choices: +# 1. Directly modify this file, which is not recommend, because the +# setting will be overriden when new version container.conf released. +# 2. Use a custom config file in this conf directory: lxc.custom.conf, +# it uses the same grammer as container.conf, and will be merged +# with the default container.conf by docklet at runtime. +# +# The following is an example mounting user html directory +# lxc.mount.entry = /public/home/%USERNAME%/public_html %ROOTFS%/root/public_html none bind,rw,create=dir 0 0 +# + +#### include /usr/share/lxc/config/ubuntu.common.conf +lxc.include = /usr/share/lxc/config/ubuntu.common.conf + +############## DOCKLET CONFIG ############## + +# Setup 0 tty devices +lxc.tty = 0 + +lxc.rootfs = %ROOTFS% +lxc.utsname = %HOSTNAME% + +lxc.network.type = veth +lxc.network.name = eth0 +lxc.network.veth.pair = %LXCNAME% +lxc.network.script.up = Bridge=docklet-br VLANID=%VLANID% %LXCSCRIPT%/lxc-ifup +lxc.network.script.down = Bridge=docklet-br %LXCSCRIPT%/lxc-ifdown +lxc.network.ipv4 = %IP% +lxc.network.ipv4.gateway = %GATEWAY% +lxc.network.flags = up +lxc.network.mtu = 1420 + +lxc.cgroup.memory.limit_in_bytes = %CONTAINER_MEMORY%M +#lxc.cgroup.memory.kmem.limit_in_bytes = 512M +#lxc.cgroup.memory.soft_limit_in_bytes = 4294967296 +#lxc.cgroup.memory.memsw.limit_in_bytes = 8589934592 + +# lxc.cgroup.cpu.cfs_period_us : period time of cpu, default 100000, means 100ms +# lxc.cgroup.cpu.cfs_quota_us : quota time of this process +lxc.cgroup.cpu.cfs_quota_us = %CONTAINER_CPU% + +lxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/data %ROOTFS%/root/nfs none bind,rw,create=dir 0 0 +lxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/hosts/%CLUSTERID%.hosts %ROOTFS%/etc/hosts none bind,ro,create=file 0 0 +lxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/ssh %ROOTFS%/root/.ssh none bind,ro,create=dir 0 0 + +# setting hostname +lxc.hook.pre-start = HNAME=%HOSTNAME% %LXCSCRIPT%/lxc-prestart + +# setting nfs softlink +#lxc.hook.mount = %LXCSCRIPT%/lxc-mount diff --git a/conf/docklet.conf.template b/conf/docklet.conf.template new file mode 100644 index 0000000..1c2d161 --- /dev/null +++ b/conf/docklet.conf.template @@ -0,0 +1,133 @@ + +# ================================================== +# +# [Local config example] +# +# ================================================== + +# CLUSTER_NAME: name of host cluster, every host cluster should have +# a unique name, default is docklet-vc +# CLUSTER_NAME=docklet-vc + +# FS_PREFIX: path to store global and local data for docklet +# default is /opt/docklet. +# +# Note: $FS_PREFIX/global is for storing persistent data, e.g., +# custom container images, user data, etc. For a multi hosts +# environement, it is the mountpoint of the distributed filesystem +# that all physical hosts (master and slave) share. +# E.g., for a system with three hosts: computing hosts A and B, +# strorage host C. Host C exports its stroage filesystem through nfs +# as C:/data, then host A and B should mount C:/data to $FS_PREFIX/global. +# Please make sure that the mount is OK before launching docklet. +# +# FS_PREFIX=/opt/docklet + +# STORAGE: local storage type, file or disk, default is file +# note lvm is required for either case +# +# file : a large file simulating raw disk storing container runtime +# data, located in FS_PREFIX/local, for single machine testing purpose. +# +# disk : raw disk for storing container files, for production purpose. +# If using disk, a partition must be allocated to docklet +# - a disk device name must be specified by DISK , e.g, /dev/sdc9 +# - this device must be formatted as Linux-LVM, and initialized +# as a physical volume (pvcreate /dev/sdc9) in advance. +# TAKE CARE to ensure the disk is OK before launching docklet. +# +# STORAGE=file +# +# DISK: disk device name if STORAGE is disk +# DISK=/dev/sdc9 + +# CLUSTER_SIZE: virtual cluster size, default is 1 +# CLUSTER_SIZE=1 + +# CLUSTER_NET: cluster network ip address range, default is 172.16.0.1/16 +# CLUSTER_NET=172.16.0.1/16 + +# CONTAINER_CPU: CPU quota of container, default is 100000 +# A single CPU core has total=100000 (100ms), so the default 100000 +# mean a single container can occupy a whole core. +# For a CPU with two cores, this can be set to 200000 +# CONTAINER_CPU=100000 + +# CONTAINER_DISK: disk quota of container image upper layer, count in MB, +# default is 1000 +# CONTAINER_DISK=1000 + +# CONTAINER_MEMORY: memory quota of container, count in MB, default is 1000 +# CONTAINER_MEMORY=1000 + +# DISKPOOL_SIZE: lvm group size, count in MB, default is 5000 +# Only valid with STORAGE=file +# DISKPOOL_SIZE=5000 + +# ETCD: etcd address, default is localhost:2379 +# For a muti hosts environment, the administrator should configure how +# etcd cluster work together +# ETCD=localhost:2379 + +# NETWORK_DEVICE: specify the network interface docklet uses, +# Default is eth0 +# NETWORK_DEVICE=eth0 + +# PORTAL_URL: the public docklet portal url. for a production system, +# it should be a valid URL, like http://docklet.info +# default is MASTER_IP:PROXY_PORT +# PORTAL_URL=http://locahost:8000 + +# MASTER_IP: master listen ip, default listens on all interfaces +# MASTER_IP=0.0.0.0 + +# MASTER_PORT: master listen port, default is 9000 +# MASTER_PORT=9000 + +# WORKER_PORT: worker listen port, default is 9001 +# WORKER_PORT=9001 + +# PROXY_PORT: the access port of the public protal, default is 8000 +# it is also the listen port of configurable-http-proxy, which +# proxy connections from exteral public network to internal private +# container networks. Usually 80 is recommded for production environment. +# PROXY_PORT=8000 + +# PROXY_API_PORT: configurable-http-proxy api port, default is 8001 +# Admins can query the proxy table by calling: +# curl http://localhost:8001/api/routes +# PROXY_API_PORT=8001 + +# WEB_PORT: docklet web listening port, default is 8888 +# Note: docklet web server is located behind the docklet proxy. +# Users access docklet first through proxy, then docklet web server. +# Therefore, it is not for user direct access. In most cases, +# admins need not to change the default value. +# WEB_PORT=8888 + +# LOG_LEVEL: logging level, of DEBUG, INFO, WARNING, ERROR, CRITICAL +# default is DEBUG +# LOG_LEVEL=DEBUG + +# LOG_LIFE: how many days the logs will be kept, default is 10 +# LOG_LIFE=10 + +# WEB_LOG_LEVEL: logging level, of DEBUG, INFO, WARNING, ERROR, CRITICAL +# default is DEBUG +# WEB_LOG_LEVEL=DEBUG + +# EXTERNAL_LOGIN: whether docklet will use external account to log in +# True or False, default is False +# default: authenticate local and PAM users +# EXTERNAL_LOGIN=False + +# EMAIL_FROM_ADDRESS : the e-mail address to send activating e-mail to user +# If this address is "", no email will be sent out. +# default: "" +# EMAIL_FROM_ADDRESS="" + +# ADMIN_EMAIL_ADDRESS : when an activating request is sent, an e-mail will +# be sent to this address to remind the admin. +# If this address i "", no email will be sent to admin. +# default: "" +# ADMIN_EMAIL_ADDRESS="" diff --git a/conf/lxc-script/lxc-ifdown b/conf/lxc-script/lxc-ifdown new file mode 100755 index 0000000..5d80873 --- /dev/null +++ b/conf/lxc-script/lxc-ifdown @@ -0,0 +1,3 @@ +#!/bin/sh + +ovs-vsctl --if-exists del-port $Bridge $5 diff --git a/conf/lxc-script/lxc-ifup b/conf/lxc-script/lxc-ifup new file mode 100755 index 0000000..0bf93b7 --- /dev/null +++ b/conf/lxc-script/lxc-ifup @@ -0,0 +1,10 @@ +#!/bin/sh + + +# $1 : name of container ( name in lxc-start with -n) +# $2 : net +# $3 : network flags, up or down +# $4 : network type, for example, veth +# $5 : value of lxc.network.veth.pair + +ovs-vsctl --may-exist add-port $Bridge $5 tag=$VLANID diff --git a/conf/lxc-script/lxc-mount b/conf/lxc-script/lxc-mount new file mode 100755 index 0000000..f6b6385 --- /dev/null +++ b/conf/lxc-script/lxc-mount @@ -0,0 +1,7 @@ +#!/bin/sh + +# $1 Container name. +# $2 Section (always 'lxc'). +# $3 The hook type (i.e. 'clone' or 'pre-mount'). + +#cd $LXC_ROOTFS_PATH/root ; rm -rf nfs && ln -s ../nfs nfs diff --git a/conf/lxc-script/lxc-prestart b/conf/lxc-script/lxc-prestart new file mode 100755 index 0000000..ef9fc1d --- /dev/null +++ b/conf/lxc-script/lxc-prestart @@ -0,0 +1,8 @@ +#!/bin/sh + +# $1 Container id +# $2 Container name. +# $3 Section (always 'lxc'). +# $4 The hook type (i.e. 'clone' or 'pre-mount'). + +echo $HNAME > $LXC_ROOTFS_PATH/etc/hostname diff --git a/doc/devdoc/coding.md b/doc/devdoc/coding.md new file mode 100644 index 0000000..a40ef8f --- /dev/null +++ b/doc/devdoc/coding.md @@ -0,0 +1,93 @@ +# NOTE + +## here is some thinking and notes in coding + +* path : scripts' path should be known by scripts to call/import other script -- use environment variables + +* FS_PREFIX : docklet filesystem path to put data + +* overlay : " modprobe overlay " to add overlay module + +* after reboot : + * bridges lost -- it's ok, recreate it + * loop device lost -- losetup /dev/loop0 BLOCK_FILE again, and lvm will get group and volume back automatically + +* lvm can do snapshot, image management can use lvm's snapshot -- No! lvm snapshot will use the capacity of LVM group. + +* cgroup memory control maybe not work. need run command below: + echo 'GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"' >> /etc/default/grub && update-grub && reboot + +* debian don't support cpu.cfs_quota_us option in cgroup. it needs to recompile linux kernel with CONFIG_CFS_BANDWIDTH option + +* ip can add bridge/link/GRE, maybe we should test whether ip can replace of ovs-vsctl and brctl. ( see "man ip-link" ) + +* lxc.mount.entry : + * do not use relevant path. use absolute path, like : + lxc.mount.entry = /root/from-dir /root/rootfs/to-dir none bind 0 0 # lxc.rootfs = /root/rootfs + if use relevant paht, container path will be mounted on /usr/lib/x86_64..../ , a not existed path + * path of host and container should both exist. if not exist in container, it will be mounted on /usr/lib/x86_64.... + * if path in container not exists, you can use option : create=dir/file, like : + lxc.mount.entry = /root/from-dir /root/rootfs/to-dir none bind,create=dir 0 0 # lxc.rootfs = /root/rootfs + +* lxc.mount.entry : bind and rbind ( see "man mount" ) + * bind means mount a part of filesystem on somewhere else of this filesystem + * but bind only attachs a single filesystem. That means the submount of source directory of mount may disappear in target directory. + * if you want to make submount work, use rbind option. + rbind will make entire file hierarchy including submounts mounted on another place. + * NOW, we use bind in container.sh. maybe it need rbind if FS_PREFIX/global/users/$USERNAME/nfs is under glusterfs mountpoint + +* rpc server maybe not security. anyone can call rpc method if he knows ip address. + * maybe we can use "transport" option of xmlrpc.client.ServerProxy(uri, transport="http://user:pass@host:port/path") and SimpleXMLRPCRequestHandler of xmlrpc.server.SimpleXMLRPCServer(addr, requestHandler=..) to parse the rpc request and authenticate the request + xmlrpc.client.ServerProxy can also support https request, it is also a security method + * If we use rpc with authentication, maybe we can use http server and http request to replace rpc + +* frontend and backend + arch: + +-----------------+ + Web -- Flask --HttpRest Core | + +-----------------+ + Now, HttpRest and Core work as backend + Web and Flask work as frontend + all modules are in backend + Flask just dispatch urls and render web pages + (Maybe Flask can be merged in Core and works as http server) + (Then Flask needs to render pages, parse urls, response requests, ...) + (It maybe not fine) + +* httprest.py : + httphandler needs to call vclustermgr/nodemgr/... to handler request + we need to call these classes in httphandler + Way-1: init/new these classes in httphandler init function (httphandler need to init parent class) -- wrong : httpserver will create a new httphandler instance for every http request ( see /usr/lib/python3.4/socketserver.py ) + Way-2: use global varibles -- Now this way + +* in shell, run python script or other not built-in command, the command will run in new process and new process group ( see csapp shell lab ) + so, the environment variables set in shell can not be see in python/... + but command like below can work : + A=ab B=ba ./python.py + +* maybe we need to parse argvs in python + some module to parse argvs : sys.argv, optparse, getopt, argparse + +* in shell, { command; } means run command in current shell, ";" is necessary + ( command; ) means run command in sub shell + +* function in registered in rpc server must have return. + without return, the rpc client will raise an exception + +* ** NEED TO BE FIX ** + we add a prefix in etcdlib + so when we getkey, key may be a absolute path from base url + when we setkey use the key we get, etcdlib will append the absolute path to prefix, it will wrong + +* overlay : upperdir and workdir must in the same mount filesystem. + that means we should mount LV first and then mkdir upperdir and workdir in the LV mountpoint + +* when use 'worker.py > log' to redirect output of python script, it will empty output of log. + because python interpreter will use buffer to collect output. + we can use ways below to fix this problem: + stdbuf -o 0 worker.py > log # but it fail in my try. don't know why + python3 -u worker.py > log # recommended, -u option of python3 + print('output', flush=True) # flush option of print + sys.stdout.flush() # flush by hand + +* CPU QUOTA should not be too small. too small it will work so slowly diff --git a/doc/devdoc/config_info.md b/doc/devdoc/config_info.md new file mode 100644 index 0000000..d32e9f4 --- /dev/null +++ b/doc/devdoc/config_info.md @@ -0,0 +1,77 @@ +# Info of docklet + +## container info + container name : username-clusterid-nodeid + hostname : host-nodeid + lxc config : /var/lib/lxc/username-clusterid-nodeid/config + lxc rootfs : /var/lib/lxc/username-clusterid-nodeid/rootfs + lxc rootfs + |__ / : aufs : basefs + volume/username-clusterid-nodeid + |__ /nfs : global/users/username/data + |__ /etc/hosts : global/users/username/clusters/clusterid/hosts + |__ /root/.ssh : global/users/username/ssh + + +## ETCD Table +we use etcd for some configuration information of our clusters, here is some details. + +every cluster has a CLUSTER_NAME and all data of this cluster is put in a directory called CLUSTER_NAME in etcd just like a table. + +so, different cluster should has different CLUSTER_NAME. + +below is content of cluster info in CLUSTER_NAME 'table' in etcd: + + + key token random code token for checking whether master and workers has the same global filesystem + + dir machines ... info of physical clusters + dir machines/allnodes ip:ok record all nodes, for recovery and checks + dir machines/runnodes ip: ? record running node for this start up. + when startup: ETCD + | IP:waiting | 1. worker write worker-ip:waiting + 2. master update IP:init-mode | IP:init-mode | 3. worker init itself by init-mode + | IP:work | 4. worker finish init and update IP:work + 5. master add workerip and update IP:ok | IP:ok | + + key service/master master-ip + key service/mode new,recovery start mode of cluster + + key vcluster/nextid ID next available ID + + + +## filesystem +here is the path and content description of docklet filesystem + + FS_PREFIX + |__ global/users/{username} + | |__ clusters/clustername : clusterid, cluster size, status, containers, ... in json format + | |__ hosts/id.hosts : ip host-nodeid host-nodeid.clustername + | |__ data : direcroty in distributed filesystem for user to put his data + | |__ ssh : ssh keys + | + |__ local + |__ docklet-storage : loop file for lvm + |__ basefs : base image + |__ volume / { username-clusterid-nodeid } : upper layer of container + + + +## vcluster files + +### hosts file:(raw) + IP-0 host-0 host-0.clustername + IP-1 host-1 host-1.clustername + ... + +### info file:(json) + { + clusterid: ID , + status: stopped/running , + size: size , + containers: [ + { containername: lxc_name, hostname: hostname, ip: lxc_ip, host: host_ip }, + { containername: lxc_name, hostname: hostname, ip: lxc_ip, host: host_ip }, + ... + ] + } diff --git a/doc/devdoc/networkmgr.md b/doc/devdoc/networkmgr.md new file mode 100644 index 0000000..2f2aecc --- /dev/null +++ b/doc/devdoc/networkmgr.md @@ -0,0 +1,67 @@ +# Network Manager + +## About +网络管理是为docklet提供网络管理的模块。 + +关于需求,主要有两点: +* 一个中心管理池,按 网络段(IP/CIDR) 给用户分配网络池 +* 很多用户网络池,按 一个或者几个网络地址 给用户的cluster分配网络地址 + +## Data Structure +面对这两种需求,设计了两种数据结构来管理网络地址。 +* 区间池 / interval pool : 分配、回收 网络段 + + + interval pool 中的元素为区间,其由很多个区间组成。 + 一个朴素的 区间池 是这样的 : interval pool : [A1,A2],[B1,B2],[C1,C2],...[X1,X2] + 每次申请一段地址的时候,从上述区间中选择一个区间分配,并将该区间中剩余部分放回区间池 + + 而考虑到 网络段(IP/CIDR) 是 2 的幂的结构,所以可以将区间池进一步设计成如下结构: + interval pool: + ... ... + cidr=16 : [A1,A2], [A3,A4], ... + cidr=17 : [B1,B2], [B3,B4], ... + cidr=18 : [C1,C2], [C3,C4], ... + ... ... + 上述结构还可以进一步优化,因为 每一个区间的结尾地址可以通过开始地址和CIDR算出来,所以每个区间只需要写一个起始地址就可以了 + 所以: + interval pool: + ... ... + cidr=16 : A1, A3, ... + cidr=17 : B1, B3, ... + cidr=18 : C1, C3, ... + ... ... + 而其中,每一个元素,比如 A1,其实代表的是一个区间 [A1, A1+2^16-1] + 这种基于2的幂的区间设计的好处是可以方便的进行 分配 和 合并 区间,操作起来更加高效。 + +* 枚举池 / enumeration pool : 分配、回收一个、多个网络地址 + + + enum pool 中的元素为单个网络地址,比如: + enum pool : A, B, C, D, ... X + +## API +操作上述两种数据结构的API,这里省略 + +## Network Manager Storage Design +* center : 中心池,提供 用户网络段 的分配、回收 + + + info : IP/CIDR + intervalpool : + cidr16 : ... + cidr17 : ... + ... ... + +* system : 系统保留地址,为系统内部的 网络地址 提供 分配回收 + + + info : IP/CIDR + enumpool : ... + +* vlan/ tag= + ovs-vsctl clear port tag + +patch 是用来连接两个网桥的,操作如下: + + ovs-vsctl add-br br0 + ovs-vsctl add-br br1 + ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1 + ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0 + # NOW : two bridges are connected by patch + + +## Note 4 +一台机器上一个域的网桥只有一个,比如在 host-0 上,建两个网桥: + + ovs-vsctl add-br br0 + ip address add 172.0.0.1/8 dev br0 + ip link set br0 up + + ovs-vsctl add-br br1 + ip address add 172.0.0.2/8 dev br1 + ip link set br1 up + +则,后配置的那个网桥会失效 + +因为系统认为,172.0.0.1/8 内的机器都应该在 br0 中 + +而以下配置是正确的: + + ovs-vsctl add-br br0 + ip address add 172.0.0.1/24 dev br0 + ip link set br0 up + + ovs-vsctl add-br br1 + ip address add 172.0.1.1/24 dev br1 + ip link set br1 up + +## Note 5 +关于网关,网桥/交换机是二层设备,网关是三层组件,我们可以将网桥连接起来,多个网桥共用一个网关 + + ovs-vsctl add-br br0 + ip link set br0 up + ovs-vsctl add-br br1 + ip address add 172.0.0.1/24 dev br1 + ip link set br1 up + ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1 + ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0 + + # lxc config : + # ip -- 172.0.0.11/24 + # gateway -- 172.0.0.1 + # lxc.network.veth.pair -- base , base is connected on br0 + lxc-start -f container.conf -n base -F -- /bin/bash + # NOW : lxc network is running ok + +## Note 6 +基于多个网桥实现VLAN + +### 方案一 + + ovs-vsctl add-br br0 + ip link set br0 up + ovs-vsctl add-br br1 + ip address add 172.0.0.1/24 dev br1 + ip link set br1 up + ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1 + ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0 + + # lxc config : + # ip -- 172.0.0.11/24 + # gateway -- 172.0.0.1 + # lxc.network.veth.pair -- base , base is connected on br0 + lxc-start -f container.conf -n base -F -- /bin/bash + # NOW : lxc network is running ok + ## above is the same as before + + ovs-vsctl set port base tag=5 + ovs-vsctl set port patch0 tag=5 + # NOW : lxc network is running ok + + # ARCH + +-----------------------+ +----------------------+ + | br0 | | br1 : 172.0.0.1/24 | + +--+-----tag=5---tag=5--+ +---+-------+----------+ + | | | patch | | + | | +-------------------+ | + | | | + internal base:172.0.0.11/24 internal + (gateway:172.0.0.1) + + # flow : base --> patch --> br1/internal + +* 方案可行 +* 但是,每个 VLAN 需要一个网关 + +### 方案二 (不可行) + + # ARCH + +-------------------------------------------------------------+ + | br0 | + +--+-----tag=5---tag=5---------+-----tag=6---tag=6---------+--+ + | | | +-----+ | | | +-----+ | + | | +--| br1 |--+ | +--| br2 |--+ + | | +-----+ | +-----+ + internal base1:172.0.0.11/24 base2:172.0.0.12/24 + + # flow 1 : base1 --> br1 --> internal + # flow 2 : base1 --> br1 --> br2 --> base2 + +* 方案不可行,因为上面的 flow 可以使得 base1、base2 在二层通信,无法隔离 + +## Note 7 +上述可行方案的简化版 +### 简化版一 + + ovs-vsctl add-br br0 + ip link set br0 up + # add a fake bridge connected to br0 with vlan tag=5 + ovs-vsctl add-br fakebr br0 5 + ip address add 172.0.0.1/24 dev fakebr + ip link set fakebr up + + # lxc config: + # ip : 172.0.0.11/24 + # gateway : 172.0.0.1/24 + # lxc.network.veth.pair -- base , base is connected on br0 + lxc-start -f container.conf -n base -F -- /bin/bash + + ovs-vsctl set port base tag=5 + + # ARCH + +-----------------------+ + | br0 | + +--+-----tag=5---tag=5--+ + | | | + | | fakebr:172.0.0.1/24 + | | + internal base:172.0.0.11/24 + (gateway:172.0.0.1) + + # flow : base --> fakebr + +### 简化版二 + + ovs-vsctl add-br br0 + ip link set br0 up + # add an internal interface for vlan + ovs-vsctl add-port br0 vlanif tag=5 -- set interface vlanif type=internal + ip address add 172.0.0.1/24 dev vlanif + ip link set vlanif up + + # lxc config: + # ip : 172.0.0.11/24 + # gateway : 172.0.0.1/24 + # lxc.network.veth.pair -- base , base is connected on br0 + lxc-start -f container.conf -n base -F -- /bin/bash + + ovs-vsctl set port base tag=5 + + # ARCH + +-----------------------+ + | br0 | + +--+-----tag=5---tag=5--+ + | | | + | | vlanif:172.0.0.1/24 + | | + internal base:172.0.0.11/24 + (gateway:172.0.0.1) + + # flow : base --> vlanif + +### 简化版一 & 简化版二 +使用 ovs-vsctl show 查看的时候,上述两个版本显示的信息是一样的,说明 fakebr 其实本质上可能就是一个 internal interface + +其实,方案一中,对 br1 的 IP(172.0.0.1/24)的配置,其实就是对 br1 的 internal 的 interface 的配置,所以其实多余的网桥不是必须的,而 interface 才是真正需要的。 + +而,internal interface 相当于是连接着本地Linux的虚拟网卡,这块网卡的另一端连着OVS的虚拟网桥。 + +而,Linux 的网络栈又管理着物理网卡、虚拟网卡,以及对这些网卡的包进行转发、路由等处理。 + +似乎,Linux 的网络栈又成了一个大的交换机/网桥,上面连接着 internal interface 和 物理网卡。 + +## Note 8 +基于上述的实践和探索,其实 **我们需要给一个VLAN配置一个可以出去的网关、网卡。** + +那么,我们一个简单可行的方案可以这样: + + +------------------------------------------------------------------------------+ + | bridge | + | <------- VLAN ID=5 ---------> <---- VLAN ID=6 ------> | + +--+-----tag=5---tag=5------------tag=5-------------tag=6-------------tag=6----+ + | | | | | | + | | lxc-2:172.0.0.12/24 | | | + internal | (gateway:172.0.0.1) | | | + | | | | + lxc-1:172.0.0.11/24 gw5:172.0.0.1/24 lxc-3:172.0.1.11/24 gw6:172.0.1.1/24 + (gateway:172.0.0.1) internal (gateway:172.0.1.1) internal + | | + | | + +----------- NAT / iptables --------+ + |||| + |||| + \\\/// + \\// + \/ + + + + +# end diff --git a/doc/devdoc/proxy-control.md b/doc/devdoc/proxy-control.md new file mode 100644 index 0000000..3817cbc --- /dev/null +++ b/doc/devdoc/proxy-control.md @@ -0,0 +1,33 @@ +# Some Note for configurable-http-proxy usage + +## intsall + sudo apt-get install nodejs nodejs-legacy npm + sudo npm install -g configurable-http-proxy + +## start + configurable-http-proxy -h : for help + configurable-http-proxy --ip IP \ + --port PORT \ + --api-ip IP \ + --api-port PORT \ + --default-target http://IP:PORT \ + --log-level debug/info/warn/error +default ip:port is 0.0.0.0:8000, +default api-ip:api-port is localhost:8001 + +## control route table +### get route table +* without token: + curl http://localhost:8001/api/routes +* with token: + curl -H "Authorization: token TOKEN" http://localhost:8001/api/routes +### add/set route table +* without token: + curl -XPOST --data '{"target":"http://TARGET-IP:TARGET-PORT"}' http://localhost:8001/api/routes/PROXY-URL +* with token: + curl -H "Authorization: token TOKEN" -XPOST --data '{"target":"http://TARGET-IP:TARGET-PORT"}' http://localhost:8001/api/routes/PROXY-URL +### delete route table line +* without token: + curl -XDELETE http://localhost:8001/api/routes/PROXY-URL +* with token: + curl -H "Authorization: token TOKEN" -XDELETE http://localhost:8001/api/routes/PROXY-URL diff --git a/doc/devdoc/startup.md b/doc/devdoc/startup.md new file mode 100644 index 0000000..697d1ef --- /dev/null +++ b/doc/devdoc/startup.md @@ -0,0 +1,45 @@ +# startup mode + +## new mode +#### step 1 : data + + clean etcd table + write token + init etcd table + clean global directory of user clusters +#### step 2 : nodemgr + + init network + wait for all nodes starts + |_____ listen node joins IP:waiting <--- worker starts + update etcd ----> IP:init-mode ---> worker init + |____ stop all containers + |____ umount mountpoint, delete lxc files, delete LV + |____ delete VG, umount loop dev, delete loop file + |____ init loop file, loop dev, create VG + add node to list <--- IP:work <---- init done, begin work + check all nodes begin work +#### step 3 : vclustermgr + Nothing to do + + + + +## recovery mode +#### step 1 : data + + write token + init some of etcd table +#### step 2 : nodemgr + + init network + wait for all nodes starts + |_____ listen node joins IP:waiting <--- worker starts + update etcd ----> IP:init-mode ---> worker init + |____ check loop file, loop dev, VG + |____ check all containers and mountpoint + add node to list <--- IP:work <---- init done, begin work + check all nodes begin work +#### step 3 : vclustermgr + + recover vclusters:some need start ---------------> recover containers: some need start diff --git a/doc/example/example-LogisticRegression.py b/doc/example/example-LogisticRegression.py new file mode 100644 index 0000000..37fac09 --- /dev/null +++ b/doc/example/example-LogisticRegression.py @@ -0,0 +1,40 @@ +# import package +import numpy as np +import matplotlib.pyplot as plt +from sklearn import linear_model, datasets +%matplotlib inline + +# load data : we only use target==0 and target==1 (2 types classify) and feature 0 and feature 2 () +iris = datasets.load_iris() +X = iris.data[iris.target!=2][:, [0,2]] +Y = iris.target[iris.target!=2] + +h = .02 # step size in the mesh + +logreg = linear_model.LogisticRegression(C=1e5) +logreg.fit(X, Y) + +# Plot the decision boundary. For that, we will assign a color to each +# point in the mesh [x_min, m_max]x[y_min, y_max]. +x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5 +y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5 +xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h)) +Z = logreg.predict(np.c_[xx.ravel(), yy.ravel()]) + +# Put the result into a color plot +Z = Z.reshape(xx.shape) +#plt.figure(1, figsize=(4, 3)) +plt.pcolormesh(xx, yy, Z, cmap=plt.cm.Paired) +plt.xlabel('Sepal length') +plt.ylabel('Sepal width') + +# Plot also the training points +plt.scatter(X[:, 0], X[:, 1], c=Y, edgecolors='k', cmap=plt.cm.Paired) +plt.xlabel('Sepal length') +plt.ylabel('Sepal width') + +plt.xlim(xx.min(), xx.max()) +plt.ylim(yy.min(), yy.max()) +plt.xticks(()) +plt.yticks(()) + diff --git a/prepare.sh b/prepare.sh new file mode 100755 index 0000000..c85a199 --- /dev/null +++ b/prepare.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +################################################## +# before-start.sh +# when you first use docklet, you should run this script to +# check and prepare the environment +# *important* : you need run this script again and again till success +################################################## + +if [[ "`whoami`" != "root" ]]; then + echo "FAILED: Require root previledge !" > /dev/stderr + exit 1 +fi + +# check cgroup control +which cgm &> /dev/null || { echo "FAILED : cgmanager is required, please install cgmanager" && exit 1; } +cpucontrol=$(cgm listkeys cpu) +[[ -z $(echo $cpucontrol | grep cfs_quota_us) ]] && echo "FAILED : cpu.cfs_quota_us of cgroup is not supported, you may need to recompile kernel" && exit 1 +memcontrol=$(cgm listkeys memory) +if [[ -z $(echo $memcontrol | grep limit_in_bytes) ]]; then + echo "FAILED : memory.limit_in_bytes of cgroup is not supported" + echo "Try : " + echo -e " echo 'GRUB_CMDLINE_LINUX=\"cgroup_enable=memory swapaccount=1\"' >> /etc/default/grub; update-grub; reboot" > /dev/stderr + echo "Info : if not success, you may need to recompile kernel" + exit 1 +fi + +# install packages that docklet needs (in ubuntu) +# some packages' name maybe different in debian +apt-get install -y cgmanager lxc lvm2 bridge-utils curl exim4 openssh-server openvswitch-switch +apt-get install -y python3 python3-netifaces python3-flask python3-flask-sqlalchemy python3-pampy +apt-get install -y python3-psutil +apt-get install -y python3-lxc +apt-get install -y python3-requests python3-suds +apt-get install -y nodejs nodejs-legacy npm +apt-get install -y etcd + +# check and install configurable-http-proxy +which configurable-http-proxy &>/dev/null || npm install -g configurable-http-proxy +which configurable-http-proxy &>/dev/null || { echo "Error : install configurable-http-proxy failed, you should try again" && exit 1; } + +[[ -f conf/docklet.conf ]] || { echo "Generating docklet.conf from template" && cp conf/docklet.conf.template conf/docklet.conf; } + +echo "" +echo "All preparation installation is done." +echo "****************************************" +echo "* Please Read Lines Below Before Start *" +echo "****************************************" +echo "" + +echo "Before staring : you need a basefs image. " +echo "basefs images are provided at: " +echo " http://docklet.unias.org/download" +echo "Please download it to FS_PREFIX/local and then extract it. (defalut FS_PRERIX is /opt/docklet)" +echo "Probably you will get a dicectory structure like" +echo " /opt/docklet/local/basefs/etc " +echo " /opt/docklet/local/basefs/bin " +echo " /opt/docklet/local/basefs/..." +echo " " + +echo "Next, make sure exim4 can deliver mail out. To enable, run:" +echo "dpkg-reconfigure exim4-config" +echo "select internet site" + +echo "" + + +echo "Then start docklet as described in README.md" + diff --git a/src/container.py b/src/container.py new file mode 100755 index 0000000..59e1f95 --- /dev/null +++ b/src/container.py @@ -0,0 +1,348 @@ +#!/usr/bin/python3 + +import subprocess, os, json +import imagemgr +from log import logger +import env +from lvmtool import * + +class Container(object): + def __init__(self, addr, etcdclient): + self.addr = addr + self.etcd=etcdclient + self.libpath = env.getenv('DOCKLET_LIB') + self.confpath = env.getenv('DOCKLET_CONF') + self.fspath = env.getenv('FS_PREFIX') + # set jupyter running dir in container + self.rundir = "/home/jupyter" + # set root running dir in container + self.nodehome = "/root" + + self.lxcpath = "/var/lib/lxc" + self.imgmgr = imagemgr.ImageMgr() + + def create_container(self, lxc_name, username, user_info, clustername, clusterid, hostname, ip, gateway, vlanid, image): + logger.info("create container %s of %s for %s" %(lxc_name, clustername, username)) + try: + user_info = json.loads(user_info) + cpu = user_info["data"]["groupinfo"]["cpu"] + memory = user_info["data"]["groupinfo"]["memory"] + image = json.loads(image) + status = self.imgmgr.prepareFS(username,image,lxc_name) + if not status: + return [False, "Create container failed when preparing filesystem, possibly insufficient space"] + + #Ret = subprocess.run([self.libpath+"/lxc_control.sh", + # "create", lxc_name, username, str(clusterid), hostname, + # ip, gateway, str(vlanid), str(cpu), str(memory)], stdout=subprocess.PIPE, + # stderr=subprocess.STDOUT,shell=False, check=True) + + rootfs = "/var/lib/lxc/%s/rootfs" % lxc_name + + if not os.path.isdir("%s/global/users/%s" % (self.fspath,username)): + logger.error("user %s directory not found" % username) + return [False, "user directory not found"] + sys_run("mkdir -p /var/lib/lxc/%s" % lxc_name) + logger.info("generate config file for %s" % lxc_name) + + if os.path.exists(self.confpath+"/lxc.custom.conf"): + conffile = open(self.confpath+"/lxc.custom.conf",'r') + else: + conffile = open(self.confpath+"/container.conf",'r') + + conftext = conffile.read() + conffile.close() + conftext = conftext.replace("%ROOTFS%",rootfs) + conftext = conftext.replace("%HOSTNAME%",hostname) + conftext = conftext.replace("%IP%",ip) + conftext = conftext.replace("%GATEWAY%",gateway) + conftext = conftext.replace("%CONTAINER_MEMORY%",str(memory)) + conftext = conftext.replace("%CONTAINER_CPU%",str(cpu)) + conftext = conftext.replace("%FS_PREFIX%",self.fspath) + conftext = conftext.replace("%USERNAME%",username) + conftext = conftext.replace("%CLUSTERID%",str(clusterid)) + conftext = conftext.replace("%LXCSCRIPT%",env.getenv("LXC_SCRIPT")) + conftext = conftext.replace("%LXCNAME%",lxc_name) + conftext = conftext.replace("%VLANID%",str(vlanid)) + conftext = conftext.replace("%CLUSTERNAME%", clustername) + + conffile = open("/var/lib/lxc/%s/config" % lxc_name,"w") + conffile.write(conftext) + conffile.close() + + #logger.debug(Ret.stdout.decode('utf-8')) + logger.info("create container %s success" % lxc_name) + + # get AUTH COOKIE URL for jupyter + [status, authurl] = self.etcd.getkey("web/authurl") + if not status: + [status, masterip] = self.etcd.getkey("service/master") + if status: + webport = env.getenv("WEB_PORT") + authurl = "http://%s:%s/jupyter" % (masterip, + webport) + else: + logger.error ("get AUTH COOKIE URL failed for jupyter") + authurl = "error" + if (username=='guest'): + cookiename='guest-cookie' + else: + cookiename='docklet-jupyter-cookie' + + rundir = self.lxcpath+'/'+lxc_name+'/rootfs' + self.rundir + + logger.debug(rundir) + + if not os.path.exists(rundir): + os.makedirs(rundir) + else: + if not os.path.isdir(rundir): + os.remove(rundir) + os.makedirs(rundir) + + jconfigpath = rundir + '/jupyter.config' + config = open(jconfigpath, 'w') + jconfigs="""USER=%s +PORT=%d +COOKIE_NAME=%s +BASE_URL=%s +HUB_PREFIX=%s +HUB_API_URL=%s +IP=%s +""" % (username, 10000, cookiename, '/go/'+username+'/'+clustername, '/jupyter', + authurl, ip.split('/')[0]) + config.write(jconfigs) + config.close() + + except subprocess.CalledProcessError as sube: + logger.error('create container %s failed: %s' % (lxc_name, + sube.stdout.decode('utf-8'))) + return [False, "create container failed"] + except Exception as e: + logger.error(e) + return [False, "create container failed"] + return [True, "create container success"] + + def delete_container(self, lxc_name): + logger.info ("delete container:%s" % lxc_name) + if self.imgmgr.deleteFS(lxc_name): + logger.info("delete container %s success" % lxc_name) + return [True, "delete container success"] + else: + logger.info("delete container %s failed" % lxc_name) + return [False, "delete container failed"] + #status = subprocess.call([self.libpath+"/lxc_control.sh", "delete", lxc_name]) + #if int(status) == 1: + # logger.error("delete container %s failed" % lxc_name) + # return [False, "delete container failed"] + #else: + # logger.info ("delete container %s success" % lxc_name) + # return [True, "delete container success"] + + # start container, if running, restart it + def start_container(self, lxc_name): + logger.info ("start container:%s" % lxc_name) + #status = subprocess.call([self.libpath+"/lxc_control.sh", "start", lxc_name]) + #if int(status) == 1: + # logger.error ("start container %s failed" % lxc_name) + # return [False, "start container failed"] + #else: + # logger.info ("start container %s success" % lxc_name) + # return [True, "start container success"] + #subprocess.run(["lxc-stop -k -n %s" % lxc_name], + # stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, check=True) + try : + subprocess.run(["lxc-start -n %s" % lxc_name], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, check=True) + logger.info ("start container %s success" % lxc_name) + return [True, "start container success"] + except subprocess.CalledProcessError as sube: + logger.error('start container %s failed: %s' % (lxc_name, + sube.stdout.decode('utf-8'))) + return [False, "start container failed"] + + # start container services + # for the master node, jupyter must be started, + # for other node, ssh must be started. + # container must be RUNNING before calling this service + def start_services(self, lxc_name, services=[]): + logger.info ("start services for container %s: %s" % (lxc_name, services)) + try: + #Ret = subprocess.run(["lxc-attach -n %s -- ln -s /nfs %s" % + #(lxc_name, self.nodehome)], + #stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + #shell=True, check=False) + #logger.debug ("prepare nfs for %s: %s" % (lxc_name, + #Ret.stdout.decode('utf-8'))) + # not sure whether should execute this + #Ret = subprocess.run(["lxc-attach -n %s -- service ssh start" % lxc_name], + # stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + #shell=True, check=False) + #logger.debug(Ret.stdout.decode('utf-8')) + if len(services) == 0: # master node + Ret = subprocess.run(["lxc-attach -n %s -- su -c %s/start_jupyter.sh" % (lxc_name, self.rundir)], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True, check=True) + logger.debug (Ret) + logger.info ("start services for container %s success" % lxc_name) + return [True, "start container services success"] + except subprocess.CalledProcessError as sube: + logger.error('start services for container %s failed: %s' % (lxc_name, + sube.output.decode('utf-8'))) + return [False, "start services for container failed"] + + # recover container: if running, do nothing. if stopped, start it + def recover_container(self, lxc_name): + logger.info ("recover container:%s" % lxc_name) + #status = subprocess.call([self.libpath+"/lxc_control.sh", "status", lxc_name]) + [success, status] = self.container_status(lxc_name) + if not success: + return [False, status] + if status == 'stopped': + logger.info("%s stopped, recover it to running" % lxc_name) + if self.start_container(lxc_name)[0]: + if self.start_services(lxc_name)[0]: + logger.info("%s recover success" % lxc_name) + return [True, "recover success"] + else: + logger.error("%s recover failed with services not start" % lxc_name) + return [False, "recover failed for services not start"] + else: + logger.error("%s recover failed for container starting failed" % lxc_name) + return [False, "recover failed for container starting failed"] + else: + logger.info("%s recover success" % lxc_name) + return [True, "recover success"] + + def stop_container(self, lxc_name): + logger.info ("stop container:%s" % lxc_name) + #status = subprocess.call([self.libpath+"/lxc_control.sh", "stop", lxc_name]) + [success, status] = self.container_status(lxc_name) + if not success: + return [False, status] + if status == "running": + sys_run("lxc-stop -k -n %s" % lxc_name) + [success, status] = self.container_status(lxc_name) + if status == "running": + logger.error("stop container %s failed" % lxc_name) + return [False, "stop container failed"] + else: + logger.info("stop container %s success" % lxc_name) + return [True, "stop container success"] + #if int(status) == 1: + # logger.error ("stop container %s failed" % lxc_name) + # return [False, "stop container failed"] + #else: + # logger.info ("stop container %s success" % lxc_name) + # return [True, "stop container success"] + + # check container: check LV and mountpoints, if wrong, try to repair it + def check_container(self, lxc_name): + logger.info ("check container:%s" % lxc_name) + if not check_volume("docklet-group", lxc_name): + logger.error("check container %s failed" % lxc_name) + return [False, "check container failed"] + #status = subprocess.call([self.libpath+"/lxc_control.sh", "check", lxc_name]) + self.imgmgr.checkFS(lxc_name) + logger.info ("check container %s success" % lxc_name) + return [True, "check container success"] + + def is_container(self, lxc_name): + if os.path.isdir(self.lxcpath+"/"+lxc_name): + return True + else: + return False + + def container_status(self, lxc_name): + if not self.is_container(lxc_name): + return [False, "container not found"] + Ret = sys_run("lxc-info -n %s | grep RUNNING") + #status = subprocess.call([self.libpath+"/lxc_control.sh", "status", lxc_name]) + if Ret.returncode == 0: + return [True, 'running'] + else: + return [True, 'stopped'] + + def list_containers(self): + if not os.path.isdir(self.lxcpath): + return [True, []] + lxclist = [] + for onedir in os.listdir(self.lxcpath): + if os.path.isfile(self.lxcpath+"/"+onedir+"/config"): + lxclist.append(onedir) + else: + logger.warning ("%s in lxc directory, but not container directory" % onedir) + return [True, lxclist] + + def delete_allcontainers(self): + logger.info ("deleting all containers...") + [status, containers] = self.list_containers() + result = True + for container in containers: + [result, status] = self.container_status(container) + if status=='running': + self.stop_container(container) + result = result & self.delete_container(container)[0] + if result: + logger.info ("deleted all containers success") + return [True, 'all deleted'] + else: + logger.error ("deleted all containers failed") + return [False, 'some containers delete failed'] + + # list containers in /var/lib/lxc/ as local + # list containers in FS_PREFIX/global/... on this host as global + def diff_containers(self): + [status, localcontainers] = self.list_containers() + globalpath = self.fspath+"/global/users/" + users = os.listdir(globalpath) + globalcontainers = [] + for user in users: + clusters = os.listdir(globalpath+user+"/clusters") + for cluster in clusters: + clusterfile = open(globalpath+user+"/clusters/"+cluster, 'r') + clusterinfo = json.loads(clusterfile.read()) + for container in clusterinfo['containers']: + if container['host'] == self.addr: + globalcontainers.append(container['containername']) + both = [] + onlylocal = [] + onlyglobal = [] + for container in localcontainers: + if container in globalcontainers: + both.append(container) + else: + onlylocal.append(container) + for container in globalcontainers: + if container not in localcontainers: + onlyglobal.append(container) + return [both, onlylocal, onlyglobal] + + def create_image(self,username,imagename,containername,description="not thing",isforce = False): + return self.imgmgr.createImage(username,imagename,containername,description,isforce) + + def flush_container(self,username,imagename,containername): + self.imgmgr.flush_one(username,imagename,containername) + logger.info("container: %s has been flushed" % containername) + return 0 + # check all local containers + def check_allcontainers(self): + [both, onlylocal, onlyglobal] = self.diff_containers() + logger.info("check all containers and repair them") + status = True + result = True + for container in both: + logger.info ("%s in LOCAL and GLOBAL checks..." % container) + [status, meg]=self.check_container(container) + result = result & status + if len(onlylocal) > 0: + result = False + logger.error ("some container only exists in LOCAL: %s" % onlylocal) + if len(onlyglobal) > 0: + result = False + logger.error ("some container only exists in GLOBAL: %s" % onlyglobal) + if status: + logger.info ("check all containers success") + return [True, 'all is ok'] + else: + logger.error ("check all containers failed") + return [False, 'not ok'] diff --git a/src/env.py b/src/env.py new file mode 100755 index 0000000..77c66bd --- /dev/null +++ b/src/env.py @@ -0,0 +1,54 @@ +import os + +def getenv(key): + if key == "CLUSTER_NAME": + return os.environ.get("CLUSTER_NAME", "docklet-vc") + elif key == "FS_PREFIX": + return os.environ.get("FS_PREFIX", "/opt/docklet") + elif key == "CLUSTER_SIZE": + return int(os.environ.get("CLUSTER_SIZE", 1)) + elif key == "CLUSTER_NET": + return os.environ.get("CLUSTER_NET", "172.16.0.1/16") + elif key == "CONTAINER_CPU": + return int(os.environ.get("CONTAINER_CPU", 100000)) + elif key == "CONTAINER_DISK": + return int(os.environ.get("CONTAINER_DISK", 1000)) + elif key == "CONTAINER_MEMORY": + return int(os.environ.get("CONTAINER_MEMORY", 1000)) + elif key == "DISKPOOL_SIZE": + return int(os.environ.get("DISKPOOL_SIZE", 5000)) + elif key == "ETCD": + return os.environ.get("ETCD", "localhost:2379") + elif key == "NETWORK_DEVICE": + return os.environ.get("NETWORK_DEVICE", "eth0") + elif key == "MASTER_IP": + return os.environ.get("MASTER_IP", "0.0.0.0") + elif key == "MASTER_PORT": + return int(os.environ.get("MASTER_PORT", 9000)) + elif key == "WORKER_PORT": + return int(os.environ.get("WORKER_PORT", 9001)) + elif key == "PROXY_PORT": + return int(os.environ.get("PROXY_PORT", 8000)) + elif key == "PROXY_API_PORT": + return int(os.environ.get("PROXY_API_PORT", 8001)) + elif key == "WEB_PORT": + return int(os.environ.get("WEB_PORT", 8888)) + elif key == "PORTAL_URL": + return os.environ.get("PORTAL_URL", + "http://"+getenv("MASTER_IP") + ":" + str(getenv("PROXY_PORT"))) + elif key == "LOG_LEVEL": + return os.environ.get("LOG_LEVEL", "DEBUG") + elif key == "LOG_LIFE": + return int(os.environ.get("LOG_LIFE", 10)) + elif key == "WEB_LOG_LEVEL": + return os.environ.get("WEB_LOG_LEVEL", "DEBUG") + elif key == "STORAGE": + return os.environ.get("STORAGE", "file") + elif key =="EXTERNAL_LOGIN": + return os.environ.get("EXTERNAL_LOGIN", "False") + elif key =="EMAIL_FROM_ADDRESS": + return os.environ.get("EMAIL_FROM_ADDRESS", "") + elif key =="ADMIN_EMAIL_ADDRESS": + return os.environ.get("ADMIN_EMAIL_ADDRESS", "") + else: + return os.environ[key] diff --git a/src/etcdlib.py b/src/etcdlib.py new file mode 100755 index 0000000..757c6a2 --- /dev/null +++ b/src/etcdlib.py @@ -0,0 +1,202 @@ +#!/usr/bin/python3 + +############################################################ +# etcdlib.py -- etcdlib provides a python etcd client +# author : Bao Li , UniAS, SEI, PKU +# license : BSD License +############################################################ + +import urllib.request, urllib.error +import random, json, time +#import sys + +# send http request to etcd server and get the json result +# url : url +# data : data to send by POST/PUT +# method : method used by http request +def dorequest(url, data = "", method = 'GET'): + try: + if method == 'GET': + response = urllib.request.urlopen(url, timeout=10).read() + else: + # use PUT/DELETE/POST, data should be encoded in ascii/bytes + request = urllib.request.Request(url, data = data.encode('ascii'), method = method) + response = urllib.request.urlopen(request, timeout=10).read() + # etcd may return json result with response http error code + # http error code will raise exception in urlopen + # catch the HTTPError and get the json result + except urllib.error.HTTPError as e: + # e.fp must be read() in this except block. + # the e will be deleted and e.fp will be closed after this block + response = e.fp.read() + # response is encoded in bytes. + # recoded in utf-8 and loaded in json + result = json.loads(str(response, encoding='utf-8')) + return result + + +# client to use etcd +# not all APIs are implemented below. just implement what we want +class Client(object): + # server is a string of one server IP and PORT, like 192.168.4.12:2379 + def __init__(self, server, prefix = ""): + self.clientid = str(random.random()) + self.server = "http://"+server + prefix = prefix.strip("/") + if prefix == "": + self.keysurl = self.server+"/v2/keys/" + else: + self.keysurl = self.server+"/v2/keys/"+prefix+"/" + self.members = self.getmembers() + + def getmembers(self): + out = dorequest(self.server+"/v2/members") + result = [] + for one in out['members']: + result.append(one['clientURLs'][0]) + return result + + # list etcd servers + def listmembers(self): + return self.members + + def clean(self): + [baseurl, dirname] = self.keysurl.split("/v2/keys/", maxsplit=1) + dirname = dirname.strip("/") + if dirname == '': # clean root content + [status, result] = self.listdir("") + if status: + for one in result: + if 'dir' in one: + self.deldir(one['key']) + else: + self.delkey(one['key']) + if self.isdir("_lock"): + self.deldir("_lock") + else: # clean a directory + if self.isdir("")[0]: + self.deldir("") + self.createdir("") + + def getkey(self, key): + key = key.strip("/") + out = dorequest(self.keysurl+key) + if 'action' not in out: + return [False, "key not found"] + else: + return [True, out['node']['value']] + + def setkey(self, key, value, ttl=0): + key = key.strip("/") + if ttl == 0: + out = dorequest(self.keysurl+key, 'value='+str(value), 'PUT') + else: + out = dorequest(self.keysurl+key, 'value='+str(value)+"&ttl="+str(ttl), 'PUT') + if 'action' not in out: + return [False, 'set key failed'] + else: + return [True, out['node']['value']] + + def delkey(self, key): + key = key.strip("/") + out = dorequest(self.keysurl+key, method='DELETE') + if 'action' not in out: + return [False, 'delete key failed'] + else: + return [True, out['node']['key']] + + def isdir(self, dirname): + dirname = dirname.strip("/") + out = dorequest(self.keysurl+dirname) + if 'action' not in out: + return [False, dirname+" not found"] + if 'dir' not in out['node']: + return [False, dirname+" is a key"] + return [True, dirname] + + def createdir(self, dirname): + dirname = dirname.strip("/") + out = dorequest(self.keysurl+dirname, 'dir=true', 'PUT') + if 'action' not in out: + return [False, 'create dir failed'] + else: + return [True, out['node']['key']] + + # list key-value in the directory. BUT not recursive. + # if necessary, recursive can be supported by add ?recursive=true in url + def listdir(self, dirname): + dirname = dirname.strip("/") + out = dorequest(self.keysurl+dirname) + if 'action' not in out: + return [False, 'list directory failed'] + else: + if "dir" not in out['node']: + return [False, dirname+" is a key"] + if 'nodes' not in out['node']: + return [True, []] + result=[] + for kv in out['node']['nodes']: + if 'dir' in kv: + result.append({"key":kv['key'], 'dir':True}) + else: + result.append({"key":kv['key'], 'value':kv['value']}) + return [True, result] + + # del directory with recursive=true + def deldir(self, dirname): + dirname = dirname.strip("/") + out = dorequest(self.keysurl+dirname+"?recursive=true", method='DELETE') + if 'action' not in out: + return [False, 'delete directory failed'] + else: + return [True, out['node']['key']] + + # watch a key or directory when it changes. + # recursive=true means anything in the directory changes, it will return + def watch(self, key): + key = key.strip("/") + out = dorequest(self.keysurl+key+"?wait=true&recursive=true") + if 'action' not in out: + return [False, 'watch key failed'] + else: + return [True, out['node']['value']] + + # atomic create a key. return immediately with True or False + def atomiccreate(self, key, value='atom'): + key = key.strip("/") + out = dorequest(self.keysurl+key+"?prevExist=false", 'value='+value, method='PUT') + if 'action' not in out: + return [False, 'atomic create key failed'] + else: + return [True, out['node']['key']] + + ################# Lock ################## + # lockref(key) : get a reference of a lock named key in etcd. + # not need to create this lock. it is automatical. + # acquire(lockref) : acquire this lock by lockref. + # blocked if lock is holded by others + # release(lockref) : release this lock by lockref + # only can be released by holder + ######################################### + def lockref(self, key): + key = key.strip("/") + return "_lock/"+key + + def acquire(self, lockref): + while(True): + if self.atomiccreate(lockref, self.clientid)[0]: + return [True, 'get lock'] + else: + time.sleep(0.01) + + def release(self, lockref): + value = self.getkey(lockref) + if value[0]: + if value[1] == self.clientid: + self.delkey(lockref) + return [True, 'release lock'] + else: + return [False, 'you are not lock holder'] + else: + return [False, 'no one holds this lock'] + diff --git a/src/guest_control.py b/src/guest_control.py new file mode 100755 index 0000000..73c238e --- /dev/null +++ b/src/guest_control.py @@ -0,0 +1,36 @@ +#!/usr/bin/python3 + +import os,time,subprocess +import env +import json + +class Guest(object): + def __init__(self,vclusterMgr,nodemgr): + self.libpath = env.getenv('DOCKLET_LIB') + self.fspath = env.getenv('FS_PREFIX') + self.lxcpath = "/var/lib/lxc" + self.G_vclustermgr = vclusterMgr + self.nodemgr = nodemgr + + def work(self): + image = {} + image['name'] = "base" + image['type'] = "base" + image['owner'] = "docklet" + while len(self.nodemgr.get_rpcs()) < 1: + time.sleep(10) + if not os.path.isdir(self.fspath+"/global/users/guest"): + subprocess.getoutput(self.libpath+"/userinit.sh guest") + user_info = {} + user_info["data"] = {} + user_info["data"]["groupinfo"] = {} + user_info["data"]["groupinfo"]["cpu"] = 100000 + user_info["data"]["groupinfo"]["memory"] = 2000 + user_info = json.dumps(user_info) + self.G_vclustermgr.create_cluster("guestspace", "guest", image, user_info) + while True: + self.G_vclustermgr.start_cluster("guestspace", "guest") + time.sleep(3600) + self.G_vclustermgr.stop_cluster("guestspace", "guest") + fspath = self.fspath + "/global/local/volume/guest-1-0/" + subprocess.getoutput("(cd %s && rm -rf *)" % fspath) diff --git a/src/httprest.py b/src/httprest.py new file mode 100755 index 0000000..08f11f9 --- /dev/null +++ b/src/httprest.py @@ -0,0 +1,585 @@ +#!/usr/bin/python3 + +# load environment variables in the beginning +# because some modules need variables when import +# for example, userManager/model.py + +# must first init loadenv +import tools, env +config = env.getenv("CONFIG") +tools.loadenv(config) + +# second init logging +# must import logger after initlogging, ugly +from log import initlogging +initlogging("docklet-master") +from log import logger + +import os +import http.server, cgi, json, sys, shutil +from socketserver import ThreadingMixIn +import nodemgr, vclustermgr, etcdlib, network, imagemgr +import userManager +import monitor +import guest_control, threading + +external_login = env.getenv('EXTERNAL_LOGIN') +if (external_login == 'TRUE'): + from userDependence import external_auth + +class DockletHttpHandler(http.server.BaseHTTPRequestHandler): + def response(self, code, output): + self.send_response(code) + self.send_header("Content-type", "application/json") + self.end_headers() + # wfile/rfile are in byte/binary encoded. need to recode + self.wfile.write(json.dumps(output).encode('ascii')) + self.wfile.write("\n".encode('ascii')) + # do not wfile.close() + # because self.handle_one_request will call wfile.flush after calling do_* + # and self.handle_one_request will close this wfile after timeout automatically + # (see /usr/lib/python3.4/http/server.py handle_one_request function) + #self.wfile.close() + + # override log_request to not print default request log + # we use the log info by ourselves in our style + def log_request(code = '-', size = '-'): + pass + + def do_PUT(self): + self.response(400, {'success':'false', 'message':'Not supported methond'}) + + def do_GET(self): + self.response(400, {'success':'false', 'message':'Not supported methond'}) + + def do_DELETE(self): + self.response(400, {'success':'false', 'message':'Not supported methond'}) + + # handler POST request + def do_POST(self): + global G_vclustermgr + global G_usermgr + #logger.info ("get request, header content:\n%s" % self.headers) + #logger.info ("read request content:\n%s" % self.rfile.read(int(self.headers["Content-Length"]))) + logger.info ("get request, path: %s" % self.path) + # for test + if self.path == '/test': + logger.info ("return welcome for test") + self.response(200, {'success':'true', 'message':'welcome to docklet'}) + return [True, 'test ok'] + + # check for not null content + if 'Content-Length' not in self.headers: + logger.info ("request content is null") + self.response(401, {'success':'false', 'message':'request content is null'}) + return [False, 'content is null'] + + # auth the user + # cgi.FieldStorage need fp/headers/environ. (see /usr/lib/python3.4/cgi.py) + form = cgi.FieldStorage(fp=self.rfile, headers=self.headers,environ={'REQUEST_METHOD':'POST'}) + cmds = self.path.strip('/').split('/') + if cmds[0] == 'register' and form.getvalue('activate', None) == None: + logger.info ("handle request : user register") + username = form.getvalue('username', '') + password = form.getvalue('password', '') + email = form.getvalue('email', '') + description = form.getvalue('description','') + if (username == '' or password == '' or email == ''): + self.response(500, {'success':'false'}) + newuser = G_usermgr.newuser() + newuser.username = form.getvalue('username') + newuser.password = form.getvalue('password') + newuser.e_mail = form.getvalue('email') + newuser.student_number = form.getvalue('studentnumber') + newuser.department = form.getvalue('department') + newuser.nickname = form.getvalue('truename') + newuser.truename = form.getvalue('truename') + newuser.description = form.getvalue('description') + newuser.status = "init" + newuser.auth_method = "local" + result = G_usermgr.register(user = newuser) + self.response(200, result) + return [True, "register succeed"] + if cmds[0] == 'login': + logger.info ("handle request : user login") + user = form.getvalue("user") + key = form.getvalue("key") + if user == None or key == None: + self.response(401, {'success':'false', 'message':'user or key is null'}) + return [False, "auth failed"] + auth_result = G_usermgr.auth(user, key) + if auth_result['success'] == 'false': + self.response(401, {'success':'false', 'message':'auth failed'}) + return [False, "auth failed"] + self.response(200, {'success':'true', 'action':'login', 'data': auth_result['data']}) + return [True, "auth succeeded"] + if cmds[0] == 'external_login': + logger.info ("handle request : external user login") + try: + result = G_usermgr.auth_external(form) + self.response(200, result) + return result + except: + result = {'success': 'false', 'reason': 'Something wrong happened when auth an external account'} + self.response(200, result) + return result + + token = form.getvalue("token") + if token == None: + self.response(401, {'success':'false', 'message':'user or key is null'}) + return [False, "auth failed"] + cur_user = G_usermgr.auth_token(token) + if cur_user == None: + self.response(401, {'success':'false', 'message':'token failed or expired', 'Unauthorized': 'True'}) + return [False, "auth failed"] + + + + user = cur_user.username + # parse the url and get to do actions + # /cluster/list + # /cluster/create & clustername + # /cluster/start & clustername + # /cluster/stop & clustername + # /cluster/delete & clustername + # /cluster/info & clustername + + + if cmds[0] == 'cluster': + clustername = form.getvalue('clustername') + # check for 'clustername' : all actions except 'list' need 'clustername' + if (cmds[1] != 'list') and clustername == None: + self.response(401, {'success':'false', 'message':'clustername is null'}) + return [False, "clustername is null"] + if cmds[1] == 'create': + image = {} + image['name'] = form.getvalue("imagename") + image['type'] = form.getvalue("imagetype") + image['owner'] = form.getvalue("imageowner") + user_info = G_usermgr.selfQuery(cur_user = cur_user) + user_info = json.dumps(user_info) + logger.info ("handle request : create cluster %s with image %s " % (clustername, image['name'])) + [status, result] = G_vclustermgr.create_cluster(clustername, user, image, user_info) + if status: + self.response(200, {'success':'true', 'action':'create cluster', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'create cluster', 'message':result}) + elif cmds[1] == 'scaleout': + logger.info("handle request : scale out %s" % clustername) + image = {} + image['name'] = form.getvalue("imagename") + image['type'] = form.getvalue("imagetype") + image['owner'] = form.getvalue("imageowner") + logger.debug("imagename:" + image['name']) + logger.debug("imagetype:" + image['type']) + logger.debug("imageowner:" + image['owner']) + user_info = G_usermgr.selfQuery(cur_user = cur_user) + user_info = json.dumps(user_info) + [status, result] = G_vclustermgr.scale_out_cluster(clustername, user, image, user_info) + if status: + self.response(200, {'success':'true', 'action':'scale out', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'scale out', 'message':result}) + elif cmds[1] == 'scalein': + logger.info("handle request : scale in %s" % clustername) + containername = form.getvalue("containername") + [status, result] = G_vclustermgr.scale_in_cluster(clustername, user, containername) + if status: + self.response(200, {'success':'true', 'action':'scale in', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'scale in', 'message':result}) + elif cmds[1] == 'start': + logger.info ("handle request : start cluster %s" % clustername) + [status, result] = G_vclustermgr.start_cluster(clustername, user) + if status: + self.response(200, {'success':'true', 'action':'start cluster', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'start cluster', 'message':result}) + elif cmds[1] == 'stop': + logger.info ("handle request : stop cluster %s" % clustername) + [status, result] = G_vclustermgr.stop_cluster(clustername, user) + if status: + self.response(200, {'success':'true', 'action':'stop cluster', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'stop cluster', 'message':result}) + elif cmds[1] == 'delete': + logger.info ("handle request : delete cluster %s" % clustername) + [status, result] = G_vclustermgr.delete_cluster(clustername, user) + if status: + self.response(200, {'success':'true', 'action':'delete cluster', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'delete cluster', 'message':result}) + elif cmds[1] == 'info': + logger.info ("handle request : info cluster %s" % clustername) + [status, result] = G_vclustermgr.get_clusterinfo(clustername, user) + if status: + self.response(200, {'success':'true', 'action':'info cluster', 'message':result}) + else: + self.response(200, {'success':'false', 'action':'info cluster', 'message':result}) + elif cmds[1] == 'list': + logger.info ("handle request : list clusters for %s" % user) + [status, clusterlist] = G_vclustermgr.list_clusters(user) + if status: + self.response(200, {'success':'true', 'action':'list cluster', 'clusters':clusterlist}) + else: + self.response(400, {'success':'false', 'action':'list cluster', 'message':clusterlist}) + + elif cmds[1] == 'flush': + from_lxc = form.getvalue('from_lxc') + G_vclustermgr.flush_cluster(user,clustername,from_lxc) + self.response(200, {'success':'true', 'action':'flush'}) + + elif cmds[1] == 'save': + imagename = form.getvalue("image") + description = form.getvalue("description") + containername = form.getvalue("containername") + isforce = form.getvalue("isforce") + if isforce == "true": + isforce = True + else: + isforce = False + [status,message] = G_vclustermgr.create_image(user,clustername,containername,imagename,description,isforce) + if status: + logger.info("image has been saved") + self.response(200, {'success':'true', 'action':'save'}) + else: + logger.debug(message) + self.response(400, {'success':'false', 'message':message}) + + else: + logger.warning ("request not supported ") + self.response(400, {'success':'false', 'message':'not supported request'}) + + # Request for Image + elif cmds[0] == 'image': + if cmds[1] == 'list': + images = G_imagemgr.list_images(user) + self.response(200, {'success':'true', 'images': images}) + elif cmds[1] == 'description': + image = {} + image['name'] = form.getvalue("imagename") + image['type'] = form.getvalue("imagetype") + image['owner'] = form.getvalue("imageowner") + description = G_imagemgr.get_image_description(user,image) + self.response(200, {'success':'true', 'message':description}) + elif cmds[1] == 'share': + image = form.getvalue('image') + G_imagemgr.shareImage(user,image) + self.response(200, {'success':'true', 'action':'share'}) + elif cmds[1] == 'unshare': + image = form.getvalue('image') + G_imagemgr.unshareImage(user,image) + self.response(200, {'success':'true', 'action':'unshare'}) + elif cmds[1] == 'delete': + image = form.getvalue('image') + G_imagemgr.removeImage(user,image) + self.response(200, {'success':'true', 'action':'delete'}) + else: + logger.warning("request not supported ") + self.response(400, {'success':'false', 'message':'not supported request'}) + + # Add Proxy + elif cmds[0] == 'addproxy': + logger.info ("handle request : add proxy") + proxy_ip = form.getvalue("ip") + proxy_port = form.getvalue("port") + clustername = form.getvalue("clustername") + [status, message] = G_vclustermgr.addproxy(user,clustername,proxy_ip,proxy_port) + if status is True: + self.response(200, {'success':'true', 'action':'addproxy'}) + else: + self.response(400, {'success':'false', 'message': message}) + # Delete Proxy + elif cmds[0] == 'deleteproxy': + logger.info ("handle request : delete proxy") + clustername = form.getvalue("clustername") + G_vclustermgr.deleteproxy(user,clustername) + self.response(200, {'success':'true', 'action':'deleteproxy'}) + + # Request for Monitor + elif cmds[0] == 'monitor': + logger.info("handle request: monitor") + res = {} + if cmds[1] == 'hosts': + com_id = cmds[2] + fetcher = monitor.Fetcher(etcdaddr,G_clustername,com_id) + if cmds[3] == 'meminfo': + res['meminfo'] = fetcher.get_meminfo() + elif cmds[3] == 'cpuinfo': + res['cpuinfo'] = fetcher.get_cpuinfo() + elif cmds[3] == 'cpuconfig': + res['cpuconfig'] = fetcher.get_cpuconfig() + elif cmds[3] == 'diskinfo': + res['diskinfo'] = fetcher.get_diskinfo() + elif cmds[3] == 'osinfo': + res['osinfo'] = fetcher.get_osinfo() + elif cmds[3] == 'containers': + res['containers'] = fetcher.get_containers() + elif cmds[3] == 'status': + res['status'] = fetcher.get_status() + elif cmds[3] == 'containerslist': + res['containerslist'] = fetcher.get_containerslist() + elif cmds[3] == 'containersinfo': + res = [] + conlist = fetcher.get_containerslist() + for container in conlist: + ans = {} + confetcher = monitor.Container_Fetcher(etcdaddr,G_clustername) + ans = confetcher.get_basic_info(container) + ans['cpu_use'] = confetcher.get_cpu_use(container) + ans['mem_use'] = confetcher.get_mem_use(container) + res.append(ans) + else: + self.response(400, {'success':'false', 'message':'not supported request'}) + return + + self.response(200, {'success':'true', 'monitor':res}) + elif cmds[1] == 'vnodes': + fetcher = monitor.Container_Fetcher(etcdaddr,G_clustername) + if cmds[3] == 'cpu_use': + res['cpu_use'] = fetcher.get_cpu_use(cmds[2]) + elif cmds[3] == 'mem_use': + res['mem_use'] = fetcher.get_mem_use(cmds[2]) + elif cmds[3] == 'basic_info': + res['basic_info'] = fetcher.get_basic_info(cmds[2]) + self.response(200, {'success':'true', 'monitor':res}) + elif cmds[1] == 'user': + if not user == 'root': + self.response(400, {'success':'false', 'message':'Root Required'}) + if cmds[3] == 'clustercnt': + flag = True + clutotal = 0 + clurun = 0 + contotal = 0 + conrun = 0 + [status, clusterlist] = G_vclustermgr.list_clusters(cmds[2]) + if status: + for clustername in clusterlist: + clutotal += 1 + [status2, result] = G_vclustermgr.get_clusterinfo(clustername, cmds[2]) + if status2: + contotal += result['size'] + if result['status'] == 'running': + clurun += 1 + conrun += result['size'] + else: + flag = False + if flag: + res = {} + res['clutotal'] = clutotal + res['clurun'] = clurun + res['contotal'] = contotal + res['conrun'] = conrun + self.response(200, {'success':'true', 'monitor':{'clustercnt':res}}) + else: + self.response(200, {'success':'false','message':clusterlist}) + elif cmds[3] == 'cluster': + if cmds[4] == 'list': + [status, clusterlist] = G_vclustermgr.list_clusters(cmds[2]) + if status: + self.response(200, {'success':'true', 'monitor':{'clusters':clusterlist}}) + else: + self.response(400, {'success':'false', 'message':clusterlist}) + elif cmds[4] == 'info': + clustername = form.getvalue('clustername') + logger.info ("handle request : info cluster %s" % clustername) + [status, result] = G_vclustermgr.get_clusterinfo(clustername, user) + if status: + self.response(200, {'success':'true', 'monitor':{'info':result}}) + else: + self.response(200, {'success':'false','message':result}) + else: + self.response(400, {'success':'false', 'message':'not supported request'}) + + elif cmds[1] == 'listphynodes': + res['allnodes'] = G_nodemgr.get_allnodes() + self.response(200, {'success':'true', 'monitor':res}) + # Request for User + elif cmds[0] == 'user': + logger.info("handle request: user") + if cmds[1] == 'modify': + #user = G_usermgr.query(username = form.getvalue("username"), cur_user = cur_user).get('token', None) + result = G_usermgr.modify(newValue = form, cur_user = cur_user) + self.response(200, result) + if cmds[1] == 'groupModify': + result = G_usermgr.groupModify(newValue = form, cur_user = cur_user) + self.response(200, result) + if cmds[1] == 'query': + result = G_usermgr.query(ID = form.getvalue("ID"), cur_user = cur_user) + if (result.get('success', None) == None or result.get('success', None) == "false"): + self.response(301,result) + else: + result = G_usermgr.queryForDisplay(user = result['token']) + self.response(200,result) + + elif cmds[1] == 'add': + user = G_usermgr.newuser(cur_user = cur_user) + user.username = form.getvalue('username') + user.password = form.getvalue('password') + user.e_mail = form.getvalue('e_mail', '') + user.status = "normal" + result = G_usermgr.register(user = user, cur_user = cur_user) + self.response(200, result) + elif cmds[1] == 'groupadd': + result = G_usermgr.groupadd(name = form.getvalue('name', None), cur_user = cur_user) + self.response(200, result) + elif cmds[1] == 'data': + logger.info("handle request: user/data") + result = G_usermgr.userList(cur_user = cur_user) + self.response(200, result) + elif cmds[1] == 'groupNameList': + result = G_usermgr.groupListName(cur_user = cur_user) + self.response(200, result) + elif cmds[1] == 'groupList': + result = G_usermgr.groupList(cur_user = cur_user) + self.response(200, result) + elif cmds[1] == 'groupQuery': + result = G_usermgr.groupQuery(ID = form.getvalue("ID", '3'), cur_user = cur_user) + if (result.get('success', None) == None or result.get('success', None) == "false"): + self.response(301,result) + else: + self.response(200,result) + elif cmds[1] == 'selfQuery': + result = G_usermgr.selfQuery(cur_user = cur_user) + self.response(200,result) + elif cmds[1] == 'selfModify': + result = G_usermgr.selfModify(cur_user = cur_user, newValue = form) + self.response(200,result) + elif cmds[0] == 'register' : + #activate + logger.info("handle request: user/activate") + newuser = G_usermgr.newuser() + newuser.username = cur_user.username + newuser.nickname = cur_user.truename + newuser.status = 'applying' + newuser.user_group = cur_user.user_group + newuser.auth_method = cur_user.auth_method + newuser.e_mail = form.getvalue('email','') + newuser.student_number = form.getvalue('studentnumber', '') + newuser.department = form.getvalue('department', '') + newuser.truename = form.getvalue('truename', '') + newuser.tel = form.getvalue('tel', '') + newuser.description = form.getvalue('description', '') + result = G_usermgr.register(user = newuser) + userManager.send_remind_activating_email(newuser.username) + self.response(200,result) + else: + logger.warning ("request not supported ") + self.response(400, {'success':'false', 'message':'not supported request'}) + +class ThreadingHttpServer(ThreadingMixIn, http.server.HTTPServer): + pass + +if __name__ == '__main__': + global G_nodemgr + global G_vclustermgr + global G_usermgr + global etcdclient + global G_networkmgr + global G_clustername + # move 'tools.loadenv' to the beginning of this file + + fs_path = env.getenv("FS_PREFIX") + logger.info("using FS_PREFIX %s" % fs_path) + + etcdaddr = env.getenv("ETCD") + logger.info("using ETCD %s" % etcdaddr) + + G_clustername = env.getenv("CLUSTER_NAME") + logger.info("using CLUSTER_NAME %s" % G_clustername) + + # get network interface + net_dev = env.getenv("NETWORK_DEVICE") + logger.info("using NETWORK_DEVICE %s" % net_dev) + + ipaddr = network.getip(net_dev) + if ipaddr==False: + logger.error("network device is not correct") + sys.exit(1) + else: + logger.info("using ipaddr %s" % ipaddr) + + # init etcdlib client + try: + etcdclient = etcdlib.Client(etcdaddr, prefix = G_clustername) + except Exception: + logger.error ("connect etcd failed, maybe etcd address not correct...") + sys.exit(1) + mode = 'recovery' + if len(sys.argv) > 1 and sys.argv[1] == "new": + mode = 'new' + + # do some initialization for mode: new/recovery + if mode == 'new': + # clean and initialize the etcd table + if etcdclient.isdir(""): + etcdclient.clean() + else: + etcdclient.createdir("") + token = tools.gen_token() + tokenfile = open(fs_path+"/global/token", 'w') + tokenfile.write(token) + tokenfile.write("\n") + tokenfile.close() + etcdclient.setkey("token", token) + etcdclient.setkey("service/master", ipaddr) + etcdclient.setkey("service/mode", mode) + etcdclient.createdir("machines/allnodes") + etcdclient.createdir("machines/runnodes") + etcdclient.setkey("vcluster/nextid", "1") + # clean all users vclusters files : FS_PREFIX/global/users//clusters/ + usersdir = fs_path+"/global/users/" + for user in os.listdir(usersdir): + shutil.rmtree(usersdir+user+"/clusters") + shutil.rmtree(usersdir+user+"/hosts") + os.mkdir(usersdir+user+"/clusters") + os.mkdir(usersdir+user+"/hosts") + else: + # check whether cluster exists + if not etcdclient.isdir("")[0]: + logger.error ("cluster not exists, you should use mode:new ") + sys.exit(1) + # initialize the etcd table for recovery + token = tools.gen_token() + tokenfile = open(fs_path+"/global/token", 'w') + tokenfile.write(token) + tokenfile.write("\n") + tokenfile.close() + etcdclient.setkey("token", token) + etcdclient.setkey("service/master", ipaddr) + etcdclient.setkey("service/mode", mode) + if etcdclient.isdir("_lock")[0]: + etcdclient.deldir("_lock") + if etcdclient.isdir("machines/runnodes")[0]: + etcdclient.deldir("machines/runnodes") + etcdclient.createdir("machines/runnodes") + + G_usermgr = userManager.userManager('root') + clusternet = env.getenv("CLUSTER_NET") + logger.info("using CLUSTER_NET %s" % clusternet) + + G_networkmgr = network.NetworkMgr(clusternet, etcdclient, mode) + G_networkmgr.printpools() + + # start NodeMgr and NodeMgr will wait for all nodes to start ... + G_nodemgr = nodemgr.NodeMgr(G_networkmgr, etcdclient, addr = ipaddr, mode=mode) + logger.info("nodemgr started") + G_vclustermgr = vclustermgr.VclusterMgr(G_nodemgr, G_networkmgr, etcdclient, ipaddr, mode) + logger.info("vclustermgr started") + G_imagemgr = imagemgr.ImageMgr() + logger.info("imagemgr started") + Guest_control = guest_control.Guest(G_vclustermgr,G_nodemgr) + logger.info("guest control started") + threading.Thread(target=Guest_control.work, args=()).start() + + logger.info("startting to listen on: ") + masterip = env.getenv('MASTER_IP') + logger.info("using MASTER_IP %s", masterip) + + masterport = env.getenv('MASTER_PORT') + logger.info("using MASTER_PORT %d", int(masterport)) + +# server = http.server.HTTPServer((masterip, masterport), DockletHttpHandler) + server = ThreadingHttpServer((masterip, int(masterport)), DockletHttpHandler) + logger.info("starting master server") + server.serve_forever() diff --git a/src/imagemgr.py b/src/imagemgr.py new file mode 100755 index 0000000..663f75d --- /dev/null +++ b/src/imagemgr.py @@ -0,0 +1,284 @@ +#!/usr/bin/python3 + +""" +design: + 1. When user create an image, it will upload to an image server, at the same time, local host + will save an image. A time file will be made with them. Everytime a container start by this + image, the time file will update. + 2. When user save an image, if it is a update option, it will faster than create a new image. + 3. At image server and every physical host, run a shell script to delete the image, which is + out of time. + 4. We can show every user their own images and the images are shared by other. User can new a + cluster or scale out a new node by them. And user can remove his own images. + 5. When a remove option occur, the image server will delete it. But some physical host may + also maintain it. I think it doesn't matter. + 6. The manage of lvm has been including in this module. +""" + + +from configparser import ConfigParser +from io import StringIO +import os,sys,subprocess,time,re,datetime,threading + +from log import logger +import env +from lvmtool import * + +class ImageMgr(): + def sys_call(self,command): + output = subprocess.getoutput(command).strip() + return None if output == '' else output + + def sys_return(self,command): + return_value = subprocess.call(command,shell=True) + return return_value + + def __init__(self): + self.NFS_PREFIX = env.getenv('FS_PREFIX') + self.imgpath = self.NFS_PREFIX + "/global/images/" + self.srcpath = env.getenv('DOCKLET_LIB') + "/" + self.imageserver = "192.168.6.249" + + def datetime_toString(self,dt): + return dt.strftime("%Y-%m-%d %H:%M:%S") + + def string_toDatetime(self,string): + return datetime.datetime.strptime(string, "%Y-%m-%d %H:%M:%S") + + def updateinfo(self,imgpath,image,description): + image_info_file = open(imgpath+"."+image+".info",'w') + image_info_file.writelines([self.datetime_toString(datetime.datetime.now()) + "\n", "unshare"]) + image_info_file.close() + image_description_file = open(imgpath+"."+image+".description", 'w') + image_description_file.write(description) + image_description_file.close() + + def dealpath(self,fspath): + if fspath[-1:] == "/": + return self.dealpath(fspath[:-1]) + else: + return fspath + + def createImage(self,user,image,lxc,description="Not thing",isforce = False): + fspath = self.NFS_PREFIX + "/local/volume/" + lxc + imgpath = self.imgpath + "private/" + user + "/" + if isforce is False: + logger.info("this save operation is not force") + if os.path.exists(imgpath+image): + return [False,"target image is exists"] + self.sys_call("mkdir -p %s" % imgpath+image) + self.sys_call("rsync -a --delete --exclude=lost+found/ --exclude=nfs/ --exclude=dev/ --exclude=mnt/ --exclude=tmp/ --exclude=media/ --exclude=proc/ --exclude=sys/ %s/ %s/" % (self.dealpath(fspath),imgpath+image)) + self.sys_call("rm -f %s" % (imgpath+"."+image+"_docklet_share")) + self.updateinfo(imgpath,image,description) + logger.info("image:%s from LXC:%s create success" % (image,lxc)) + return [True, "create image success"] + + def prepareImage(self,user,image,fspath): + imagename = image['name'] + imagetype = image['type'] + imageowner = image['owner'] + if imagename == "base" and imagetype == "base": + return + if imagetype == "private": + imgpath = self.imgpath + "private/" + user + "/" + else: + imgpath = self.imgpath + "public/" + imageowner + "/" + self.sys_call("rsync -a --delete --exclude=lost+found/ --exclude=nfs/ --exclude=dev/ --exclude=mnt/ --exclude=tmp/ --exclude=media/ --exclude=proc/ --exclude=sys/ %s/ %s/" % (imgpath+imagename,self.dealpath(fspath))) + #self.sys_call("rsync -a --delete --exclude=nfs/ %s/ %s/" % (imgpath+image,self.dealpath(fspath))) + #self.updatetime(imgpath,image) + return + + def prepareFS(self,user,image,lxc,size="1000",vgname="docklet-group"): + rootfs = "/var/lib/lxc/%s/rootfs" % lxc + layer = self.NFS_PREFIX + "/local/volume/" + lxc + #check mountpoint + Ret = sys_run("mountpoint %s" % rootfs) + if Ret.returncode == 0: + logger.info("%s not clean" % rootfs) + sys_run("umount -l %s" % rootfs) + Ret = sys_run("mountpoint %s" % layer) + if Ret.returncode == 0: + logger.info("%s not clean" % layer) + sys_run("umount -l %s" % layer) + sys_run("rm -rf %s %s" % (rootfs, layer)) + sys_run("mkdir -p %s %s" % (rootfs, layer)) + + #prepare volume + if check_volume(vgname,lxc): + logger.info("volume %s already exists, delete it") + delete_volume(vgname,lxc) + if not new_volume(vgname,lxc,size): + logger.error("volume %s create failed" % lxc) + return False + sys_run("mkfs.ext4 /dev/%s/%s" % (vgname,lxc)) + sys_run("mount /dev/%s/%s %s" %(vgname,lxc,layer)) + #self.sys_call("mountpoint %s &>/dev/null && umount -l %s" % (rootfs,rootfs)) + #self.sys_call("mountpoint %s &>/dev/null && umount -l %s" % (layer,layer)) + #self.sys_call("rm -rf %s %s && mkdir -p %s %s" % (rootfs,layer,rootfs,layer)) + #rv = self.sys_return(self.srcpath+"lvmtool.sh check volume %s %s" % (vgname,lxc)) + #if rv == 1: + # self.sys_call(self.srcpath+"lvmtool.sh newvolume %s %s %s %s" % (vgname,lxc,size,layer)) + #else: + # self.sys_call(self.srcpath+"lvmtool.sh mount volume %s %s %s" % (vgname,lxc,layer)) + #self.sys_call("mkdir -p %s/overlay %s/work" % (layer,layer)) + #self.sys_call("mount -t overlay overlay -olowerdir=%s/local/basefs,upperdir=%s/overlay,workdir=%s/work %s" % (self.NFS_PREFIX,layer,layer,rootfs)) + self.sys_call("mount -t aufs -o br=%s=rw:%s/local/basefs=ro+wh none %s/" % (layer,self.NFS_PREFIX,rootfs)) + logger.info("FS has been prepared for user:%s lxc:%s" % (user,lxc)) + #self.prepareImage(user,image,layer+"/overlay") + self.prepareImage(user,image,layer) + logger.info("image has been prepared") + return True + + def deleteFS(self,lxc,vgname="docklet-group"): + rootfs = "/var/lib/lxc/%s/rootfs" % lxc + layer = self.NFS_PREFIX + "/local/volume/" + lxc + lxcpath = "/var/lib/lxc/%s" % lxc + sys_run("lxc-stop -k -n %s" % lxc) + #check mountpoint + Ret = sys_run("mountpoint %s" % rootfs) + if Ret.returncode == 0: + sys_run("umount -l %s" % rootfs) + Ret = sys_run("mountpoint %s" % layer) + if Ret.returncode == 0: + sys_run("umount -l %s" % layer) + if check_volume(vgname, lxc): + delete_volume(vgname, lxc) + sys_run("rm -rf %s %s" % (layer,lxcpath)) + return True + + def checkFS(self, lxc, vgname="docklet-group"): + rootfs = "/var/lib/lxc/%s/rootfs" % lxc + layer = self.NFS_PREFIX + "/local/volume/" + lxc + if not os.path.isdir(layer): + sys_run("mkdir -p %s" % layer) + #check mountpoint + Ret = sys_run("mountpoint %s" % layer) + if Ret.returncode != 0: + sys_run("mount /dev/%s/%s %s" % (vgname,lxc,layer)) + Ret = sys_run("mountpoint %s" % rootfs) + if Ret.returncode != 0: + self.sys_call("mount -t aufs -o br=%s=rw:%s/local/basefs=ro+wh none %s/" % (layer,self.NFS_PREFIX,rootfs)) + return True + + + def removeImage(self,user,image): + imgpath = self.imgpath + "private/" + user + "/" + self.sys_call("rm -rf %s/" % imgpath+image) + self.sys_call("rm -f %s" % imgpath+"."+image+".info") + self.sys_call("rm -f %s" % (imgpath+"."+image+".description")) + + def shareImage(self,user,image): + imgpath = self.imgpath + "private/" + user + "/" + share_imgpath = self.imgpath + "public/" + user + "/" + image_info_file = open(imgpath+"."+image+".info", 'r') + [createtime, isshare] = image_info_file.readlines() + isshare = "shared" + image_info_file.close() + image_info_file = open(imgpath+"."+image+".info", 'w') + image_info_file.writelines([createtime, isshare]) + image_info_file.close() + self.sys_call("mkdir -p %s" % (share_imgpath + image)) + self.sys_call("rsync -a --delete %s/ %s/" % (imgpath+image,share_imgpath+image)) + self.sys_call("cp %s %s" % (imgpath+"."+image+".info",share_imgpath+"."+image+".info")) + self.sys_call("cp %s %s" % (imgpath+"."+image+".description",share_imgpath+"."+image+".description")) + + + + def unshareImage(self,user,image): + public_imgpath = self.imgpath + "public/" + user + "/" + imgpath = self.imgpath + "private/" + user + "/" + if os.path.exists(imgpath + image): + image_info_file = open(imgpath+"."+image+".info", 'r') + [createtime, isshare] = image_info_file.readlines() + isshare = "unshare" + image_info_file.close() + image_info_file = open(imgpath+"."+image+".info", 'w') + image_info_file.writelines([createtime, isshare]) + image_info_file.close() + self.sys_call("rm -rf %s/" % public_imgpath+image) + self.sys_call("rm -f %s" % public_imgpath+"."+image+".info") + self.sys_call("rm -f %s" % public_imgpath+"."+image+".description") + + + def get_image_info(self, user, image, imagetype): + if imagetype == "private": + imgpath = self.imgpath + "private/" + user + "/" + else: + imgpath = self.imgpath + "public/" + user + "/" + image_info_file = open(imgpath+"."+image+".info",'r') + time = image_info_file.readline() + image_info_file.close() + image_description_file = open(imgpath+"."+image+".description",'r') + description = image_description_file.read() + image_description_file.close() + if len(description) > 15: + description = description[:15] + "......" + return [time, description] + + def get_image_description(self, user, image): + if image['type'] == "private": + imgpath = self.imgpath + "private/" + user + "/" + else: + imgpath = self.imgpath + "public/" + image['owner'] + "/" + image_description_file = open(imgpath+"."+image['name']+".description", 'r') + description = image_description_file.read() + image_description_file.close() + return description + + def list_images(self,user): + images = {} + images["private"] = [] + images["public"] = {} + imgpath = self.imgpath + "private/" + user + "/" + private_images = self.sys_call("ls %s" % imgpath) + if private_images is not None and private_images[:3] != "ls:": + private_images = private_images.split("\n") + for image in private_images: + fimage={} + fimage["name"] = image + fimage["isshared"] = self.isshared(user,image) + [time, description] = self.get_image_info(user, image, "private") + fimage["time"] = time + fimage["description"] = description + images["private"].append(fimage) + else: + pass + imgpath = self.imgpath + "public" + "/" + public_users = self.sys_call("ls %s" % imgpath) + if public_users is not None and public_users[:3] != "ls:": + public_users = public_users.split("\n") + for public_user in public_users: + imgpath = self.imgpath + "public/" + public_user + "/" + public_images = self.sys_call("ls %s" % imgpath) + if public_images is not None and public_images[:3] != "ls:": + public_images = public_images.split("\n") + images["public"][public_user] = [] + for image in public_images: + fimage = {} + fimage["name"] = image + [time, description] = self.get_image_info(public_user, image, "public") + fimage["time"] = time + fimage["description"] = description + images["public"][public_user].append(fimage) + else: + pass + return images + + def isshared(self,user,image): + imgpath = self.imgpath + "private/" + user + "/" + image_info_file = open(imgpath+"."+image+".info",'r') + [time, isshare] = image_info_file.readlines() + image_info_file.close() + if isshare == "shared": + return "true" + else: + return "false" + +if __name__ == '__main__': + mgr = ImageMgr() + if sys.argv[1] == "prepareImage": + mgr.prepareImage(sys.argv[2],sys.argv[3],sys.argv[4]) + elif sys.argv[1] == "create": + mgr.createImage(sys.argv[2],sys.argv[3],sys.argv[4]) + else: + logger.warning("unknown option") diff --git a/src/log.py b/src/log.py new file mode 100755 index 0000000..b2985c2 --- /dev/null +++ b/src/log.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import logging +import logging.handlers +import argparse +import sys +import time # this is only being used as part of the example +import os +import env + +# logger should only be imported after initlogging has been called +logger = None + +def initlogging(name='docklet'): + # Deafults + global logger + + homepath = env.getenv('FS_PREFIX') + LOG_FILENAME = homepath + '/local/log/' + name + '.log' + + LOG_LIFE = env.getenv('LOG_LIFE') + LOG_LEVEL = env.getenv('LOG_LEVEL') + if LOG_LEVEL == "DEBUG": + LOG_LEVEL = logging.DEBUG + elif LOG_LEVEL == "INFO": + LOG_LEVEL = logging.INFO + elif LOG_LEVEL == "WARNING": + LOG_LEVEL = logging.WARNING + elif LOG_LEVEL == "ERROR": + LOG_LEVEL = logging.ERROR + elif LOG_LEVEL == "CRITICAL": + LOG_LEVEL = logging.CRITIAL + else: + LOG_LEVEL = logging.DEBUG + + logger = logging.getLogger(name) + # Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data + # Give the logger a unique name (good practice) + # Set the log level to LOG_LEVEL + logger.setLevel(LOG_LEVEL) + # Make a handler that writes to a file, making a new file at midnight and keeping 3 backups + handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, + when="midnight", backupCount=LOG_LIFE) + # Format each log message like this + formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(module)s[%(lineno)d] %(message)s') + # Attach the formatter to the handler + handler.setFormatter(formatter) + # Attach the handler to the logger + logger.addHandler(handler) + + # Replace stdout with logging to file at INFO level + sys.stdout = RedirectLogger(logger, logging.INFO) + # Replace stderr with logging to file at ERROR level + sys.stderr = RedirectLogger(logger, logging.ERROR) + + # Make a class we can use to capture stdout and sterr in the log +class RedirectLogger(object): + def __init__(self, logger, level): + """Needs a logger and a logger level.""" + self.logger = logger + self.level = level + + def write(self, message): + # Only log if there is a message (not just a new line) + if message.rstrip() != "": + self.logger.log(self.level, message.rstrip()) + + def flush(self): + for handler in self.logger.handlers: + handler.flush() diff --git a/src/lvmtool.py b/src/lvmtool.py new file mode 100755 index 0000000..152f83e --- /dev/null +++ b/src/lvmtool.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 + +import env,subprocess,os,time +from log import logger + +def sys_run(command): + Ret = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, shell=True, check=False) + return Ret + +def new_group(group_name, size = "5000", file_path = "/opt/docklet/local/docklet-storage"): + storage = env.getenv("STORAGE") + logger.info("begin initialize lvm group:%s with size %sM" % (group_name,size)) + if storage == "file": + #check vg + Ret = sys_run("vgdisplay " + group_name) + if Ret.returncode == 0: + logger.info("lvm group: " + group_name + " already exists, delete it") + Ret = sys_run("vgremove -f " + group_name) + if Ret.returncode != 0: + logger.error("delete VG %s failed:%s" % (group_name,Ret.stdout.decode('utf-8'))) + #check pv + Ret = sys_run("pvdisplay /dev/loop0") + if Ret.returncode == 0: + Ret = sys_run("pvremove -ff /dev/loop0") + if Ret.returncode != 0: + logger.error("remove pv failed:%s" % Ret.stdout.decode('utf-8')) + #check mountpoint + Ret = sys_run("losetup /dev/loop0") + if Ret.returncode == 0: + logger.info("/dev/loop0 already exists, detach it") + Ret = sys_run("losetup -d /dev/loop0") + if Ret.returncode != 0: + logger.error("losetup -d failed:%s" % Ret.stdout.decode('utf-8')) + #check file_path + if os.path.exists(file_path): + logger.info(file_path + " for lvm group already exists, delete it") + os.remove(file_path) + if not os.path.isdir(file_path[:file_path.rindex("/")]): + os.makedirs(file_path[:file_path.rindex("/")]) + sys_run("dd if=/dev/zero of=%s bs=1M seek=%s count=0" % (file_path,size)) + sys_run("losetup /dev/loop0 " + file_path) + sys_run("vgcreate %s /dev/loop0" % group_name) + logger.info("initialize lvm group:%s with size %sM success" % (group_name,size)) + return True + + elif storage == "disk": + disk = env.getenv("DISK") + if disk is None: + logger.error("use disk for story without a physical disk") + return False + #check vg + Ret = sys_run("vgdisplay " + group_name) + if Ret.returncode == 0: + logger.info("lvm group: " + group_name + " already exists, delete it") + Ret = sys_run("vgremove -f " + group_name) + if Ret.returncode != 0: + logger.error("delete VG %s failed:%s" % (group_name,Ret.stdout.decode('utf-8'))) + sys_run("vgcreate %s %s" % (group_name,disk)) + logger.info("initialize lvm group:%s with size %sM success" % (group_name,size)) + return True + + else: + logger.info("unknown storage type:" + storage) + return False + +def recover_group(group_name,file_path="/opt/docklet/local/docklet-storage"): + storage = env.getenv("STORAGE") + if storage == "file": + if not os.path.exists(file_path): + logger.error("%s not found, unable to recover VG" % file_path) + return False + #recover mountpoint + Ret = sys_run("losetup /dev/loop0") + if Ret.returncode != 0: + Ret = sys_run("losetup /dev/loop0 " + file_path) + if Ret.returncode != 0: + logger.error("losetup failed:%s" % Ret.stdout.decode('utf-8')) + return False + time.sleep(1) + #recover vg + Ret = sys_run("vgdisplay " + group_name) + if Ret.returncode != 0: + Ret = sys_run("vgcreate %s /dev/loop0" % group_name) + if Ret.returncode != 0: + logger.error("create VG %s failed:%s" % (group_name,Ret.stdout.decode('utf-8'))) + return False + logger.info("recover VG %s success" % group_name) + + elif storage == "disk": + disk = env.getenv("DISK") + if disk is None: + logger.error("use disk for story without a physical disk") + return False + #recover vg + Ret = sys_run("vgdisplay " + group_name) + if Ret.returncode != 0: + Ret = sys_run("vgcreate %s %s" % (group_name,disk)) + if Ret.returncode != 0: + logger.error("create VG %s failed:%s" % (group_name,Ret.stdout.decode('utf-8'))) + return False + logger.info("recover VG %s success" % group_name) + +def new_volume(group_name,volume_name,size): + Ret = sys_run("lvdisplay %s/%s" % (group_name,volume_name)) + if Ret.returncode == 0: + logger.info("logical volume already exists, delete it") + Ret = sys_run("lvremove -f %s/%s" % (group_name,volume_name)) + if Ret.returncode != 0: + logger.error("delete logical volume %s failed: %s" % + (volume_name, Ret.stdout.decode('utf-8'))) + Ret = sys_run("lvcreate -L %sM -n %s %s" % (size,volume_name,group_name)) + if Ret.returncode != 0: + logger.error("lvcreate failed: %s" % Ret.stdout.decode('utf-8')) + return False + logger.info("create lv success") + return True + +def check_group(group_name): + Ret = sys_run("vgdisplay %s" % group_name) + if Ret.returncode == 0: + return True + else: + return False + +def check_volume(group_name,volume_name): + Ret = sys_run("lvdisplay %s/%s" % (group_name,volume_name)) + if Ret.returncode == 0: + return True + else: + return False + +def delete_group(group_name): + Ret = sys_run("vgdisplay %s" % group_name) + if Ret.returncode == 0: + Ret = sys_run("vgremove -f %s" % group_name) + if Ret.returncode == 0: + logger.info("delete vg %s success" % group_name) + return True + else: + logger.error("delete vg %s failed:%s" % (group_name,Ret.stdout.decode('utf-8'))) + return False + else: + logger.info("vg %s does not exists" % group_name) + return True + +def delete_volume(group_name, volume_name): + Ret = sys_run("lvdisplay %s/%s" % (group_name, volume_name)) + if Ret.returncode == 0: + Ret = sys_run("lvremove -f %s/%s" % (group_name, volume_name)) + if Ret.returncode == 0: + logger.info("delete lv %s in vg %s success" % (volume_name,group_name)) + return True + else: + logger.error("delete lv %s in vg %s failed:%s" % (volume_name,group_name,Ret.stdout.decode('utf-8'))) + return False + else: + logger.info("lv %s in vg %s does not exists" % (volume_name,group_name)) + + diff --git a/src/model.py b/src/model.py new file mode 100755 index 0000000..c42e4e6 --- /dev/null +++ b/src/model.py @@ -0,0 +1,144 @@ +#coding=utf-8 +''' +2 tables: users, usergroup +User: + id + username + password + avatar + nickname + description + status + student_number + department + truename + tel + e_mail + register_date + user_group + auth_method + +Usergroup + id + name + +Token expiration can be set in User.generate_auth_token +''' +from flask import Flask +from flask.ext.sqlalchemy import SQLAlchemy +from datetime import datetime +from base64 import b64encode, b64decode +import os + +#this class from itsdangerous implements token<->user +#from itsdangerous import TimedJSONWebSignatureSerializer as Serializer +from itsdangerous import JSONWebSignatureSerializer as Serializer +from itsdangerous import SignatureExpired, BadSignature + +import env + +fsdir = env.getenv('FS_PREFIX') + +app = Flask(__name__) +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+fsdir+'/local/UserTable.db' +try: + secret_key_file = open(env.getenv('FS_PREFIX') + '/local/token_secret_key.txt') + app.secret_key = secret_key_file.read() + secret_key_file.close() +except: + from os import urandom + secret_key = urandom(24) + secret_key = b64encode(secret_key).decode('utf-8') + app.secret_key = secret_key + secret_key_file = open(env.getenv('FS_PREFIX') + '/local/token_secret_key.txt', 'w') + secret_key_file.write(secret_key) + secret_key_file.close() + +db = SQLAlchemy(app) + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(10), unique=True) + password = db.Column(db.String(100)) + avatar = db.Column(db.String(30)) + nickname = db.Column(db.String(10)) + description = db.Column(db.String(15)) + status = db.Column(db.String(10)) + e_mail = db.Column(db.String(20)) + student_number = db.Column(db.String(20)) + department = db.Column(db.String(20)) + truename = db.Column(db.String(20)) + tel = db.Column(db.String(20)) + register_date = db.Column(db.String(10)) + user_group = db.Column(db.String(50)) + auth_method = db.Column(db.String(10)) + + + def __init__(self, username, password, avatar="default.png", nickname = "", description = "", status = "init", + e_mail = "" , student_number = "", department = "", truename = "", tel="", date = None, usergroup = "primary" + , auth_method = "local"): + # using sha512 + #if (len(password) <= 6): + # self = None + # return None + self.username = username + self.password = password + self.avatar = avatar + self.nickname = nickname + self.description = description + self.status = status + self.e_mail = e_mail + self.student_number = student_number + self.department = department + self.truename = truename + self.tel = tel + if (date != None): + self.register_date = date + else: + self.register_date = datetime.utcnow() + if (UserGroup.query.filter_by(name=usergroup).first() != None): + self.user_group = usergroup + else: + self.user_group = "primary" + self.auth_method = auth_method + + def __repr__(self): + return '' % self.username + + #token will expire after 3600s + # replace token with no time expiration + def generate_auth_token(self, expiration = 3600): + s = Serializer(app.config['SECRET_KEY']) + str = s.dumps({'id': self.id}) + return b64encode(str).decode('utf-8') + + @staticmethod + def verify_auth_token(token): + s = Serializer(app.config['SECRET_KEY']) + try: + data = s.loads(b64decode(token)) + except SignatureExpired: + return None # valid token, but expired + except BadSignature: + return None # invalid token + user = User.query.get(data['id']) + return user + + +class UserGroup(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(50)) + cpu = db.Column(db.String(10)) + memory = db.Column(db.String(10)) + imageQuantity = db.Column(db.String(10)) + lifeCycle = db.Column(db.String(10)) + + def __init__(self, name): + self.name = name + self.cpu = '100000' + self.memory = '2000' + self.imageQuantity = '10' + self.lifeCycle = '24' + + def __repr__(self): + return '' % self.name diff --git a/src/monitor.py b/src/monitor.py new file mode 100755 index 0000000..928eae4 --- /dev/null +++ b/src/monitor.py @@ -0,0 +1,331 @@ +#!/usr/bin/python3 + +import subprocess,re,sys,etcdlib,psutil +import time,threading,json,traceback,platform + +from log import logger + +class Container_Collector(threading.Thread): + + def __init__(self,etcdaddr,cluster_name,host,cpu_quota,mem_quota,test=False): + threading.Thread.__init__(self) + self.thread_stop = False + self.host = host + self.etcdser = etcdlib.Client(etcdaddr,"/%s/monitor" % (cluster_name)) + self.etcdser.setkey('/vnodes/cpu_quota', cpu_quota) + self.etcdser.setkey('/vnodes/mem_quota', mem_quota) + self.cpu_quota = float(cpu_quota)/100000.0 + self.mem_quota = float(mem_quota)*1000000/1024 + self.interval = 2 + self.test = test + return + + def list_container(self): + output = subprocess.check_output(["sudo lxc-ls"],shell=True) + output = output.decode('utf-8') + containers = re.split('\s+',output) + return containers + + def collect_containerinfo(self,container_name): + output = subprocess.check_output("sudo lxc-info -n %s" % (container_name),shell=True) + output = output.decode('utf-8') + parts = re.split('\n',output) + info = {} + basic_info = {} + for part in parts: + if not part == '': + key_val = re.split(':',part) + key = key_val[0] + val = key_val[1] + info[key] = val.lstrip() + basic_info['Name'] = info['Name'] + basic_info['State'] = info['State'] + if(info['State'] == 'STOPPED'): + self.etcdser.setkey('/vnodes/%s/basic_info'%(container_name), basic_info) + return False + basic_info['PID'] = info['PID'] + basic_info['IP'] = info['IP'] + self.etcdser.setkey('/vnodes/%s/basic_info'%(container_name), basic_info) + cpu_parts = re.split(' +',info['CPU use']) + cpu_val = cpu_parts[0].strip() + cpu_unit = cpu_parts[1].strip() + res = self.etcdser.getkey('/vnodes/%s/cpu_use'%(container_name)) + cpu_last = 0 + if res[0] == True: + last_use = dict(eval(res[1])) + cpu_last = float(last_use['val']) + cpu_use = {} + cpu_use['val'] = cpu_val + cpu_use['unit'] = cpu_unit + cpu_usedp = (float(cpu_val)-float(cpu_last))/(self.cpu_quota*self.interval*1.3) + if(cpu_usedp > 1): + cpu_usedp = 1 + cpu_use['usedp'] = cpu_usedp + self.etcdser.setkey('vnodes/%s/cpu_use'%(container_name), cpu_use) + mem_parts = re.split(' +',info['Memory use']) + mem_val = mem_parts[0].strip() + mem_unit = mem_parts[1].strip() + mem_use = {} + mem_use['val'] = mem_val + mem_use['unit'] = mem_unit + if(mem_unit == "MiB"): + mem_val = float(mem_val) * 1024 + mem_usedp = float(mem_val) / self.mem_quota + mem_use['usedp'] = mem_usedp + self.etcdser.setkey('/vnodes/%s/mem_use'%(container_name), mem_use) + #print(output) + #print(parts) + return True + + def run(self): + cnt = 0 + while not self.thread_stop: + containers = self.list_container() + countR = 0 + conlist = [] + for container in containers: + if not container == '': + conlist.append(container) + try: + if(self.collect_containerinfo(container)): + countR += 1 + except Exception as err: + #pass + logger.warning(err) + containers_num = len(containers)-1 + concnt = {} + concnt['total'] = containers_num + concnt['running'] = countR + self.etcdser.setkey('/hosts/%s/containers'%(self.host), concnt) + time.sleep(self.interval) + if cnt == 0: + self.etcdser.setkey('/hosts/%s/containerslist'%(self.host), conlist) + cnt = (cnt+1)%5 + if self.test: + break + return + + def stop(self): + self.thread_stop = True + + +class Collector(threading.Thread): + + def __init__(self,etcdaddr,cluster_name,host,test=False): + threading.Thread.__init__(self) + self.host = host + self.thread_stop = False + self.etcdser = etcdlib.Client(etcdaddr,"/%s/monitor/hosts/%s" % (cluster_name,host)) + self.interval = 1 + self.test=test + return + + def collect_meminfo(self): + meminfo = psutil.virtual_memory() + memdict = {} + memdict['total'] = meminfo.total/1024 + memdict['used'] = meminfo.used/1024 + memdict['free'] = meminfo.free/1024 + memdict['buffers'] = meminfo.buffers/1024 + memdict['cached'] = meminfo.cached/1024 + memdict['percent'] = meminfo.percent + self.etcdser.setkey('/meminfo',memdict) + #print(output) + #print(memparts) + return + + def collect_cpuinfo(self): + cpuinfo = psutil.cpu_times_percent(interval=1,percpu=False) + cpuset = {} + cpuset['user'] = cpuinfo.user + cpuset['system'] = cpuinfo.system + cpuset['idle'] = cpuinfo.idle + cpuset['iowait'] = cpuinfo.iowait + self.etcdser.setkey('/cpuinfo',cpuset) + output = subprocess.check_output(["cat /proc/cpuinfo"],shell=True) + output = output.decode('utf-8') + parts = output.split('\n') + info = [] + idx = -1 + for part in parts: + if not part == '': + key_val = re.split(':',part) + key = key_val[0].rstrip() + if key == 'processor': + info.append({}) + idx += 1 + val = key_val[1].lstrip() + if key=='processor' or key=='model name' or key=='core id' or key=='cpu MHz' or key=='cache size' or key=='physical id': + info[idx][key] = val + self.etcdser.setkey('/cpuconfig',info) + return + + def collect_diskinfo(self): + parts = psutil.disk_partitions() + setval = [] + devices = {} + for part in parts: + if not part.device in devices: + devices[part.device] = 1 + diskval = {} + diskval['device'] = part.device + diskval['mountpoint'] = part.mountpoint + usage = psutil.disk_usage(part.mountpoint) + diskval['total'] = usage.total + diskval['used'] = usage.used + diskval['free'] = usage.free + diskval['percent'] = usage.percent + setval.append(diskval) + self.etcdser.setkey('/diskinfo', setval) + #print(output) + #print(diskparts) + return + + def collect_osinfo(self): + uname = platform.uname() + osinfo = {} + osinfo['platform'] = platform.platform() + osinfo['system'] = uname.system + osinfo['node'] = uname.node + osinfo['release'] = uname.release + osinfo['version'] = uname.version + osinfo['machine'] = uname.machine + osinfo['processor'] = uname.processor + self.etcdser.setkey('/osinfo',osinfo) + return + + def run(self): + self.collect_osinfo() + while not self.thread_stop: + self.collect_meminfo() + self.collect_cpuinfo() + self.collect_diskinfo() + self.etcdser.setkey('/running','True',6) + time.sleep(self.interval) + if self.test: + break + # print(self.etcdser.getkey('/meminfo/total')) + return + + def stop(self): + self.thread_stop = True + +class Container_Fetcher: + def __init__(self,etcdaddr,cluster_name): + self.etcdser = etcdlib.Client(etcdaddr,"/%s/monitor/vnodes" % (cluster_name)) + return + + def get_cpu_use(self,container_name): + res = {} + [ret, ans] = self.etcdser.getkey('/%s/cpu_use'%(container_name)) + if ret == True : + res = dict(eval(ans)) + res['quota'] = self.etcdser.getkey('/cpu_quota')[1] + return res + else: + logger.warning(ans) + return res + + def get_mem_use(self,container_name): + res = {} + [ret, ans] = self.etcdser.getkey('/%s/mem_use'%(container_name)) + if ret == True : + res = dict(eval(ans)) + res['quota'] = self.etcdser.getkey('/mem_quota')[1] + return res + else: + logger.warning(ans) + return res + + def get_basic_info(self,container_name): + res = self.etcdser.getkey("/%s/basic_info"%(container_name)) + if res[0] == False: + return {} + res = dict(eval(res[1])) + return res + +class Fetcher: + + def __init__(self,etcdaddr,cluster_name,host): + self.etcdser = etcdlib.Client(etcdaddr,"/%s/monitor/hosts/%s" % (cluster_name,host)) + return + + #def get_clcnt(self): + # return DockletMonitor.clcnt + + #def get_nodecnt(self): + # return DockletMonitor.nodecnt + + #def get_meminfo(self): + # return self.get_meminfo_('172.31.0.1') + + def get_meminfo(self): + res = {} + [ret, ans] = self.etcdser.getkey('/meminfo') + if ret == True : + res = dict(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_cpuinfo(self): + res = {} + [ret, ans] = self.etcdser.getkey('/cpuinfo') + if ret == True : + res = dict(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_cpuconfig(self): + res = {} + [ret, ans] = self.etcdser.getkey('/cpuconfig') + if ret == True : + res = list(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_diskinfo(self): + res = [] + [ret, ans] = self.etcdser.getkey('/diskinfo') + if ret == True : + res = list(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_osinfo(self): + res = {} + [ret, ans] = self.etcdser.getkey('/osinfo') + if ret == True: + res = dict(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_containers(self): + res = {} + [ret, ans] = self.etcdser.getkey('/containers') + if ret == True: + res = dict(eval(ans)) + return res + else: + logger.warning(ans) + return res + + def get_status(self): + isexist = self.etcdser.getkey('/running')[0] + if(isexist): + return 'RUNNING' + else: + return 'STOPPED' + + def get_containerslist(self): + res = list(eval(self.etcdser.getkey('/containerslist')[1])) + return res diff --git a/src/nettools.py b/src/nettools.py new file mode 100755 index 0000000..e3cef28 --- /dev/null +++ b/src/nettools.py @@ -0,0 +1,276 @@ +#!/usr/bin/python3 + +import subprocess + +class ipcontrol(object): + @staticmethod + def parse(cmdout): + links = {} + thislink = None + for line in cmdout.splitlines(): + # empty line + if len(line)==0: + continue + # Level 1 : first line of one link + if line[0] != ' ': + blocks = line.split() + thislink = blocks[1].strip(':') + links[thislink] = {} + links[thislink]['state'] = blocks[blocks.index('state')+1] if 'state' in blocks else 'UNKNOWN' + # Level 2 : line with 4 spaces + elif line[4] != ' ': + blocks = line.split() + if blocks[0] == 'inet': + if 'inet' not in links[thislink]: + links[thislink]['inet'] = [] + links[thislink]['inet'].append(blocks[1]) + # we just need inet (IPv4) + else: + pass + # Level 3 or more : no need for us + else: + pass + return links + + @staticmethod + def list_links(): + try: + ret = subprocess.run(['ip', 'link', 'show'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + links = ipcontrol.parse(ret.stdout.decode('utf-8')) + return [True, list(links.keys())] + except subprocess.CalledProcessError as suberror: + return [False, "list links failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def link_exist(linkname): + try: + subprocess.run(['ip', 'link', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return True + except subprocess.CalledProcessError: + return False + + @staticmethod + def link_info(linkname): + try: + ret = subprocess.run(['ip', 'address', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, ipcontrol.parse(ret.stdout.decode('utf-8'))[str(linkname)]] + except subprocess.CalledProcessError as suberror: + return [False, "get link info failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def link_state(linkname): + try: + ret = subprocess.run(['ip', 'link', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, ipcontrol.parse(ret.stdout.decode('utf-8'))[str(linkname)]['state']] + except subprocess.CalledProcessError as suberror: + return [False, "get link state failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def link_ips(linkname): + [status, info] = ipcontrol.link_info(str(linkname)) + if status: + if 'inet' not in info: + return [True, []] + else: + return [True, info['inet']] + else: + return [False, info] + + @staticmethod + def up_link(linkname): + try: + subprocess.run(['ip', 'link', 'set', 'dev', str(linkname), 'up'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(linkname)] + except subprocess.CalledProcessError as suberror: + return [False, "set link up failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def down_link(linkname): + try: + subprocess.run(['ip', 'link', 'set', 'dev', str(linkname), 'down'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(linkname)] + except subprocess.CalledProcessError as suberror: + return [False, "set link down failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def add_addr(linkname, address): + try: + subprocess.run(['ip', 'address', 'add', address, 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(linkname)] + except subprocess.CalledProcessError as suberror: + return [False, "add address failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def del_addr(linkname, address): + try: + subprocess.run(['ip', 'address', 'del', address, 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(linkname)] + except subprocess.CalledProcessError as suberror: + return [False, "delete address failed : %s" % suberror.stdout.decode('utf-8')] + + +# ovs-vsctl list-br +# ovs-vsctl br-exists +# ovs-vsctl add-br +# ovs-vsctl del-br +# ovs-vsctl list-ports +# ovs-vsctl del-port +# ovs-vsctl add-port -- set interface type=gre options:remote_ip= +# ovs-vsctl add-port tag= -- set interface type=internal +# ovs-vsctl port-to-br +# ovs-vsctl set Port tag= +# ovs-vsctl clear Port tag + +class ovscontrol(object): + @staticmethod + def list_bridges(): + try: + ret = subprocess.run(['ovs-vsctl', 'list-br'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, ret.stdout.decode('utf-8').split()] + except subprocess.CalledProcessError as suberror: + return [False, "list bridges failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def bridge_exist(bridge): + try: + subprocess.run(['ovs-vsctl', 'br-exists', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return True + except subprocess.CalledProcessError: + return False + + @staticmethod + def port_tobridge(port): + try: + ret = subprocess.run(['ovs-vsctl', 'port-to-br', str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, ret.stdout.decode('utf-8').strip()] + except subprocess.CalledProcessError as suberror: + return [False, suberror.stdout.decode('utf-8')] + + @staticmethod + def port_exists(port): + return ovscontrol.port_tobridge(port)[0] + + @staticmethod + def add_bridge(bridge): + try: + subprocess.run(['ovs-vsctl', 'add-br', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(bridge)] + except subprocess.CalledProcessError as suberror: + return [False, "add bridge failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def del_bridge(bridge): + try: + subprocess.run(['ovs-vsctl', 'del-br', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(bridge)] + except subprocess.CalledProcessError as suberror: + return [False, "del bridge failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def list_ports(bridge): + try: + ret = subprocess.run(['ovs-vsctl', 'list-ports', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, ret.stdout.decode('utf-8').split()] + except subprocess.CalledProcessError as suberror: + return [False, "list ports failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def del_port(bridge, port): + try: + subprocess.run(['ovs-vsctl', 'del-port', str(bridge), str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(port)] + except subprocess.CalledProcessError as suberror: + return [False, "delete port failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def add_port_internal(bridge, port): + try: + subprocess.run(['ovs-vsctl', 'add-port', str(bridge), str(port), '--', 'set', 'interface', str(port), 'type=internal'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(port)] + except subprocess.CalledProcessError as suberror: + return [False, "add port failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def add_port_internal_withtag(bridge, port, tag): + try: + subprocess.run(['ovs-vsctl', 'add-port', str(bridge), str(port), 'tag='+str(tag), '--', 'set', 'interface', str(port), 'type=internal'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(port)] + except subprocess.CalledProcessError as suberror: + return [False, "add port failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def add_port_gre(bridge, port, remote): + try: + subprocess.run(['ovs-vsctl', 'add-port', str(bridge), str(port), '--', 'set', 'interface', str(port), 'type=gre', 'options:remote_ip='+str(remote)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(port)] + except subprocess.CalledProcessError as suberror: + return [False, "add port failed : %s" % suberror.stdout.decode('utf-8')] + + @staticmethod + def set_port_tag(port, tag): + try: + subprocess.run(['ovs-vsctl', 'set', 'Port', str(port), 'tag='+str(tag)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True) + return [True, str(port)] + except subprocess.CalledProcessError as suberror: + return [False, "set port tag failed : %s" % suberror.stdout.decode('utf-8')] + + +class netcontrol(object): + @staticmethod + def bridge_exists(bridge): + return ovscontrol.bridge_exist(bridge) + + @staticmethod + def del_bridge(bridge): + return ovscontrol.del_bridge(bridge) + + @staticmethod + def new_bridge(bridge): + return ovscontrol.add_bridge(bridge) + + @staticmethod + def gre_exists(bridge, remote): + # port is unique, bridge is not necessary + return ovscontrol.port_exists('gre-'+str(remote)) + + @staticmethod + def setup_gre(bridge, remote): + return ovscontrol.add_port_gre(bridge, 'gre-'+str(remote), remote) + + @staticmethod + def gw_exists(bridge, gwport): + return ovscontrol.port_exists(gwport) + + @staticmethod + def setup_gw(bridge, gwport, addr, tag): + [status, result] = ovscontrol.add_port_internal_withtag(bridge, gwport, tag) + if not status: + return [status, result] + [status, result] = ipcontrol.add_addr(gwport, addr) + if not status: + return [status, result] + return ipcontrol.up_link(gwport) + + @staticmethod + def del_gw(bridge, gwport): + return ovscontrol.del_port(bridge, gwport) + + @staticmethod + def check_gw(bridge, gwport, addr, tag): + if not netcontrol.gw_exists(bridge, gwport): + return netcontrol.setup_gw(bridge, gwport, addr, tag) + [status, info] = ipcontrol.link_info(gwport) + if not status: + return [False, "get gateway info failed"] + if ('inet' not in info) or (addr not in info['inet']): + ipcontrol.add_addr(gwport, addr) + else: + info['inet'].remove(addr) + for otheraddr in info['inet']: + ipcontrol.del_addr(gwport, otheraddr) + ovscontrol.set_port_tag(gwport, tag) + if info['state'] == 'DOWN': + ipcontrol.up_link(gwport) + return [True, "check gateway port %s" % gwport] + + diff --git a/src/network.py b/src/network.py new file mode 100755 index 0000000..24d6708 --- /dev/null +++ b/src/network.py @@ -0,0 +1,475 @@ +#!/usr/bin/python3 + +import json, sys, netifaces +from nettools import netcontrol + +from log import logger + +# getip : get ip from network interface +# ifname : name of network interface +def getip(ifname): + if ifname not in netifaces.interfaces(): + return False # No such interface + else: + addrinfo = netifaces.ifaddresses(ifname) + if 2 in addrinfo: + return netifaces.ifaddresses(ifname)[2][0]['addr'] + else: + return False # network interface is down + +def ip_to_int(addr): + [a, b, c, d] = addr.split('.') + return (int(a)<<24) + (int(b)<<16) + (int(c)<<8) + int(d) + +def int_to_ip(num): + return str((num>>24)&255)+"."+str((num>>16)&255)+"."+str((num>>8)&255)+"."+str(num&255) + +# fix addr with cidr, for example, 172.16.0.10/24 --> 172.16.0.0/24 +def fix_ip(addr, cidr): + return int_to_ip( ip_to_int(addr) & ( (-1) << (32-int(cidr)) ) ) + #return int_to_ip(ip_to_int(addr) & ( ~( (1<<(32-int(cidr)))-1 ) ) ) + +# jump to next interval address with cidr +def next_interval(addr, cidr): + addr = fix_ip(addr, int(cidr)) + return int_to_ip(ip_to_int(addr)+(1<<(32-int(cidr)))) + +# jump to before interval address with cidr +def before_interval(addr, cidr): + addr = fix_ip(addr, int(cidr)) + addrint = ip_to_int(addr)-(1<<(32-int(cidr))) + # addrint maybe negative + if addrint < 0: + return "-1.-1.-1.-1" + else: + return int_to_ip(addrint) + + +# IntervalPool : manage network blocks with IP/CIDR +# Data Structure : +# ... ... +# cidr=16 : A1, A2, ... # A1 is an IP, means an interval [A1, A1+2^16-1], equals to A1/16 +# cidr=17 : B1, B2, ... +# ... ... +# API : +# allocate +# free +class IntervalPool(object): + # cidr : 1,2, ..., 32 + def __init__(self, addr_cidr=None, copy=None): + if addr_cidr: + self.pool = {} + [addr, cidr] = addr_cidr.split('/') + cidr = int(cidr) + # fix addr with cidr, for example, 172.16.0.10/24 --> 172.16.0.0/24 + addr = fix_ip(addr, cidr) + self.info = addr+"/"+str(cidr) + # init interval pool + # cidr : [ addr ] + # cidr+1 : [ ] + # ... + # 32 : [ ] + self.pool[str(cidr)]=[addr] + for i in range(cidr+1, 33): + self.pool[str(i)]=[] + elif copy: + self.info = copy['info'] + self.pool = copy['pool'] + else: + logger.error("IntervalPool init failed with no addr_cidr or center") + + def __str__(self): + return json.dumps({'info':self.info, 'pool':self.pool}) + + def printpool(self): + cidrs = list(self.pool.keys()) + # sort with key=int(cidr) + cidrs.sort(key=int) + for i in cidrs: + print (i + " : " + str(self.pool[i])) + + # allocate an interval with CIDR + def allocate(self, thiscidr): + # thiscidr -- cidr for this request + # upcidr -- up stream which has interval to allocate + thiscidr=int(thiscidr) + upcidr = thiscidr + # find first cidr who can allocate enough ips + while((str(upcidr) in self.pool) and len(self.pool[str(upcidr)])==0): + upcidr = upcidr-1 + if str(upcidr) not in self.pool: + return [False, 'Not Enough to Allocate'] + # get the block/interval to allocate ips + upinterval = self.pool[str(upcidr)][0] + self.pool[str(upcidr)].remove(upinterval) + # split the upinterval and put the rest intervals back to interval pool + for i in range(int(thiscidr), int(upcidr), -1): + self.pool[str(i)].append(next_interval(upinterval, i)) + #self.pool[str(i)].sort(key=ip_to_int) # cidr between thiscidr and upcidr are null, no need to sort + return [True, upinterval] + + # deallocate an interval with IP/CIDR + # ToDo : when free IP/CIDR, we donot check whether IP/CIDR is in pool + # maybe we check this later + def free(self, addr, cidr): + cidr = int(cidr) + # cidr not in pool means CIDR out of pool range + if str(cidr) not in self.pool: + return [False, 'CIDR not in pool'] + addr = fix_ip(addr, cidr) + # merge interval and move to up cidr + while(True): + # cidr-1 not in pool means current CIDR is the top CIDR + if str(cidr-1) not in self.pool: + break + # if addr can satisfy cidr-1, and next_interval also exist, + # merge addr with next_interval to up cidr (cidr-1) + # if addr not satisfy cidr-1, and before_interval exist, + # merge addr with before_interval to up cidr, and interval index is before_interval + if addr == fix_ip(addr, cidr-1): + if next_interval(addr, cidr) in self.pool[str(cidr)]: + self.pool[str(cidr)].remove(next_interval(addr,cidr)) + cidr=cidr-1 + else: + break + else: + if before_interval(addr, cidr) in self.pool[str(cidr)]: + addr = before_interval(addr, cidr) + self.pool[str(cidr)].remove(addr) + cidr = cidr - 1 + else: + break + self.pool[str(cidr)].append(addr) + # sort interval with key=ip_to_int(IP) + self.pool[str(cidr)].sort(key=ip_to_int) + return [True, "Free success"] + +# EnumPool : manage network ips with ip or ip list +# Data Structure : [ A, B, C, ... X ] , A is a IP address +class EnumPool(object): + def __init__(self, addr_cidr=None, copy=None): + if addr_cidr: + self.pool = [] + [addr, cidr] = addr_cidr.split('/') + cidr=int(cidr) + addr=fix_ip(addr, cidr) + self.info = addr+"/"+str(cidr) + # init enum pool + # first IP is network id, last IP is network broadcast address + # first and last IP can not be allocated + for i in range(1, pow(2, 32-cidr)-1): + self.pool.append(int_to_ip(ip_to_int(addr)+i)) + elif copy: + self.info = copy['info'] + self.pool = copy['pool'] + else: + logger.error("EnumPool init failed with no addr_cidr or copy") + + def __str__(self): + return json.dumps({'info':self.info, 'pool':self.pool}) + + def printpool(self): + print (str(self.pool)) + + def acquire(self, num=1): + if num > len(self.pool): + return [False, "No enough IPs: %s" % self.info] + result = [] + for i in range(0, num): + result.append(self.pool.pop()) + return [True, result] + + def acquire_cidr(self, num=1): + [status, result] = self.acquire(int(num)) + if not status: + return [status, result] + return [True, list(map(lambda x:x+"/"+self.info.split('/')[1], result))] + + # ToDo : when release : + # not check whether IP is in the range of pool + # not check whether IP is already in the pool + def release(self, ip_or_ips): + if type(ip_or_ips) == str: + ips = [ ip_or_ips ] + else: + ips = ip_or_ips + for ip in ips: + # maybe ip is in format IP/CIDR + ip = ip.split('/')[0] + self.pool.append(ip) + return [True, "release success"] + +# wrap EnumPool with vlanid and gateway +class UserPool(EnumPool): + def __init__(self, addr_cidr=None, vlanid=None, copy=None): + if addr_cidr and vlanid: + EnumPool.__init__(self, addr_cidr = addr_cidr) + self.vlanid=vlanid + self.pool.sort(key=ip_to_int) + self.gateway = self.pool[0] + self.pool.remove(self.gateway) + elif copy: + EnumPool.__init__(self, copy = copy) + self.vlanid = int(copy['vlanid']) + self.gateway = copy['gateway'] + else: + logger.error("UserPool init failed with no addr_cidr or copy") + + def get_gateway(self): + return self.gateway + + def get_gateway_cidr(self): + return self.gateway+"/"+self.info.split('/')[1] + + def printpool(self): + print("users ID:"+str(self.vlanid)+", net info:"+self.info+", gateway:"+self.gateway) + print (str(self.pool)) + +# NetworkMgr : mange docklet network ip address +# center : interval pool to allocate and free network block with IP/CIDR +# system : enumeration pool to acquire and release system ip address +# users : set of users' enumeration pools to manage users' ip address +class NetworkMgr(object): + def __init__(self, addr_cidr, etcdclient, mode): + self.etcd = etcdclient + if mode == 'new': + logger.info("init network manager with %s" % addr_cidr) + self.center = IntervalPool(addr_cidr=addr_cidr) + # allocate a pool for system IPs, use CIDR=27, has 32 IPs + syscidr = 27 + [status, sysaddr] = self.center.allocate(syscidr) + if status == False: + logger.error ("allocate system ips in __init__ failed") + sys.exit(1) + # maybe for system, the last IP address of CIDR is available + # But, EnumPool drop the last IP address in its pool -- it is not important + self.system = EnumPool(sysaddr+"/"+str(syscidr)) + self.users = {} + self.vlanids = {} + self.init_vlanids(4095, 60) + self.dump_center() + self.dump_system() + elif mode == 'recovery': + logger.info("init network manager from etcd") + self.center = None + self.system = None + self.users = {} + self.vlanids = {} + self.load_center() + self.load_system() + self.load_vlanids() + else: + logger.error("mode: %s not supported" % mode) + + def init_vlanids(self, total, block): + self.vlanids['block'] = block + self.etcd.setkey("network/vlanids/info", str(total)+"/"+str(block)) + for i in range(1, int((total-1)/block)): + self.etcd.setkey("network/vlanids/"+str(i), json.dumps(list(range(1+block*(i-1), block*i+1)))) + self.vlanids['currentpool'] = list(range(1+block*i, total+1)) + self.vlanids['currentindex'] = i+1 + self.etcd.setkey("network/vlanids/"+str(i+1), json.dumps(self.vlanids['currentpool'])) + self.etcd.setkey("network/vlanids/current", str(i+1)) + + def load_vlanids(self): + [status, info] = self.etcd.getkey("network/vlanids/info") + self.vlanids['block'] = int(info.split("/")[1]) + [status, current] = self.etcd.getkey("network/vlanids/current") + self.vlanids['currentindex'] = int(current) + if self.vlanids['currentindex'] == 0: + self.vlanids['currentpool'] = [] + else: + [status, pool]= self.etcd.getkey("network/vlanids/"+str(self.vlanids['currentindex'])) + self.vlanids['currentpool'] = json.loads(pool) + + def dump_vlanids(self): + if self.vlanids['currentpool'] == []: + if self.vlanids['currentindex'] != 0: + self.etcd.delkey("network/vlanids/"+str(self.vlanids['currentindex'])) + self.etcd.setkey("network/vlanids/current", str(self.vlanids['currentindex']-1)) + else: + pass + else: + self.etcd.setkey("network/vlanids/"+str(self.vlanids['currentindex']), json.dumps(self.vlanids['currentpool'])) + + def load_center(self): + [status, centerdata] = self.etcd.getkey("network/center") + center = json.loads(centerdata) + self.center = IntervalPool(copy = center) + + def dump_center(self): + self.etcd.setkey("network/center", json.dumps({'info':self.center.info, 'pool':self.center.pool})) + + def load_system(self): + [status, systemdata] = self.etcd.getkey("network/system") + system = json.loads(systemdata) + self.system = EnumPool(copy=system) + + def dump_system(self): + self.etcd.setkey("network/system", json.dumps({'info':self.system.info, 'pool':self.system.pool})) + + def load_user(self, username): + [status, userdata] = self.etcd.getkey("network/users/"+username) + usercopy = json.loads(userdata) + user = UserPool(copy = usercopy) + self.users[username] = user + + def dump_user(self, username): + self.etcd.setkey("network/users/"+username, json.dumps({'info':self.users[username].info, 'vlanid':self.users[username].vlanid, 'gateway':self.users[username].gateway, 'pool':self.users[username].pool})) + + def printpools(self): + print ("
") + self.center.printpool() + print ("") + self.system.printpool() + print ("") + print (" users in users is in etcd, not in memory") + print ("") + print (str(self.vlanids['currentindex'])+":"+str(self.vlanids['currentpool'])) + + def acquire_vlanid(self): + if self.vlanids['currentpool'] == []: + if self.vlanids['currentindex'] == 0: + return [False, "No VLAN IDs"] + else: + logger.error("vlanids current pool is empty with current index not zero") + return [False, "internal error"] + vlanid = self.vlanids['currentpool'].pop() + self.dump_vlanids() + if self.vlanids['currentpool'] == []: + self.load_vlanids() + return [True, vlanid] + + def release_vlanid(self, vlanid): + if len(self.vlanids['currentpool']) == self.vlanids['block']: + self.vlanids['currentpool'] = [vlanid] + self.vlanids['currentindex'] = self.vanids['currentindex']+1 + self.dump_vlanids() + else: + self.vlanids['currentpool'].append(vlanid) + self.dump_vlanids() + return [True, "Release VLAN ID success"] + + def add_user(self, username, cidr): + logger.info ("add user %s with cidr=%s" % (username, str(cidr))) + if self.has_user(username): + return [False, "user already exists in users set"] + [status, result] = self.center.allocate(cidr) + self.dump_center() + if status == False: + return [False, result] + [status, vlanid] = self.acquire_vlanid() + if status: + vlanid = int(vlanid) + else: + self.center.free(result, cidr) + self.dump_center() + return [False, vlanid] + self.users[username] = UserPool(addr_cidr = result+"/"+str(cidr), vlanid=vlanid) + logger.info("setup gateway for %s with %s and vlan=%s" % (username, self.users[username].get_gateway_cidr(), str(vlanid))) + netcontrol.setup_gw('docklet-br', username, self.users[username].get_gateway_cidr(), str(vlanid)) + self.dump_user(username) + del self.users[username] + return [True, 'add user success'] + + def del_user(self, username): + logger.info ("delete user %s with cidr=%s" % (username)) + if not self.has_user(username): + return [False, username+" not in users set"] + self.load_user(username) + [addr, cidr] = self.users[username].info.split('/') + self.center.free(addr, int(cidr)) + self.dump_center() + self.release_vlanid(self.users[username].vlanid) + netcontrol.del_gw('docklet-br', username) + self.etcd.deldir("network/users/"+username) + del self.users[username] + return [True, 'delete user success'] + + def check_usergw(self, username): + self.load_user(username) + netcontrol.check_gw('docklet-br', username, self.users[username].get_gateway_cidr(), str(self.users[username].vlanid)) + del self.users[username] + return [True, 'check gw ok'] + + def has_user(self, username): + [status, _value] = self.etcd.getkey("network/users/"+username) + return status + + def acquire_userips(self, username, num=1): + logger.info ("acquire user ips of %s" % (username)) + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].acquire(num) + self.dump_user(username) + del self.users[username] + return result + + def acquire_userips_cidr(self, username, num=1): + logger.info ("acquire user ips of %s" % (username)) + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].acquire_cidr(num) + self.dump_user(username) + del self.users[username] + return result + + # ip_or_ips : one IP address or a list of IPs + def release_userips(self, username, ip_or_ips): + logger.info ("release user ips of %s with ips: %s" % (username, str(ip_or_ips))) + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].release(ip_or_ips) + self.dump_user(username) + del self.users[username] + return result + + def get_usergw(self, username): + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].get_gateway() + self.dump_user(username) + del self.users[username] + return result + + def get_usergw_cidr(self, username): + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].get_gateway_cidr() + self.dump_user(username) + del self.users[username] + return result + + def get_uservlanid(self, username): + if not self.has_user(username): + return [False, 'username not exists in users set'] + self.load_user(username) + result = self.users[username].vlanid + self.dump_user(username) + del self.users[username] + return result + + def acquire_sysips(self, num=1): + logger.info ("acquire system ips") + result = self.system.acquire(num) + self.dump_system() + return result + + def acquire_sysips_cidr(self, num=1): + logger.info ("acquire system ips") + result = self.system.acquire_cidr(num) + self.dump_system() + return result + + def release_sysips(self, ip_or_ips): + logger.info ("acquire system ips: %s" % str(ip_or_ips)) + result = self.system.release(ip_or_ips) + self.dump_system() + return result + + diff --git a/src/nodemgr.py b/src/nodemgr.py new file mode 100755 index 0000000..ffd1a09 --- /dev/null +++ b/src/nodemgr.py @@ -0,0 +1,159 @@ +#!/usr/bin/python3 + +import threading, random, time, xmlrpc.client, sys +#import network +from nettools import netcontrol +from log import logger +import env + +########################################## +# NodeMgr +# Description : manage the physical nodes +# 1. list running nodes now +# 2. update node list when new node joins +# ETCD table : +# machines/allnodes -- all nodes in docklet, for recovery +# machines/runnodes -- run nodes of this start up +############################################## +class NodeMgr(object): + def __init__(self, networkmgr, etcdclient, addr, mode): + self.addr = addr + logger.info ("begin initialize on %s" % self.addr) + self.networkmgr = networkmgr + self.etcd = etcdclient + self.mode = mode + + # initialize the network + logger.info ("initialize network") + + # 'docklet-br' not need ip address. Because every user has gateway + #[status, result] = self.networkmgr.acquire_sysips_cidr() + #self.networkmgr.printpools() + #if not status: + # logger.info ("initialize network failed, no IP for system bridge") + # sys.exit(1) + #self.bridgeip = result[0] + #logger.info ("initialize bridge wih ip %s" % self.bridgeip) + #network.netsetup("init", self.bridgeip) + + if self.mode == 'new': + if netcontrol.bridge_exists('docklet-br'): + netcontrol.del_bridge('docklet-br') + netcontrol.new_bridge('docklet-br') + else: + if not netcontrol.bridge_exists('docklet-br'): + logger.error("docklet-br not found") + sys.exit(1) + + # get allnodes + self.allnodes = self._nodelist_etcd("allnodes") + self.runnodes = self._nodelist_etcd("runnodes") + logger.info ("all nodes are: %s" % self.allnodes) + logger.info ("run nodes are: %s" % self.runnodes) + if len(self.runnodes)>0: + logger.error ("init runnodes is not null, need to be clean") + sys.exit(1) + # init rpc list + self.rpcs = [] + # start new thread to watch whether a new node joins + logger.info ("start thread to watch new nodes ...") + self.thread_watchnewnode = threading.Thread(target=self._watchnewnode) + self.thread_watchnewnode.start() + # wait for all nodes joins + while(True): + allin = True + for node in self.allnodes: + if node not in self.runnodes: + allin = False + break + if allin: + logger.info("all nodes necessary joins ...") + break + time.sleep(0.05) + logger.info ("run nodes are: %s" % self.runnodes) + + + # get nodes list from etcd table + def _nodelist_etcd(self, which): + if which == "allnodes" or which == "runnodes": + [status, nodeinfo]=self.etcd.listdir("machines/"+which) + if status: + nodelist = [] + for node in nodeinfo: + nodelist.append(node["key"].rsplit('/', 1)[1]) + return nodelist + return [] + + # thread target : watch whether a new node joins + def _watchnewnode(self): + workerport = env.getenv('WORKER_PORT') + while(True): + time.sleep(0.1) + [status, runlist] = self.etcd.listdir("machines/runnodes") + if not status: + logger.warning ("get runnodes list failed from etcd ") + continue + for node in runlist: + nodeip = node['key'].rsplit('/',1)[1] + if node['value']=='waiting': + logger.info ("%s want to joins, call it to init first" % nodeip) + # 'docklet-br' of worker do not need IP Addr. Not need to allocate an IP to it + #if nodeip != self.addr: + # [status, result] = self.networkmgr.acquire_sysips_cidr() + # self.networkmgr.printpools() + # if not status: + # logger.error("no IP for worker bridge, please check network system pool") + # continue + # bridgeip = result[0] + # self.etcd.setkey("network/workbridge", bridgeip) + if nodeip in self.allnodes: + ######## HERE MAYBE NEED TO FIX ############### + # here we must use "machines/runnodes/nodeip" + # we cannot use node['key'], node['key'] is absolute + # path, etcd client will append the path to prefix, + # which is wrong + ############################################### + self.etcd.setkey("machines/runnodes/"+nodeip, "init-"+self.mode) + else: + self.etcd.setkey('machines/runnodes/'+nodeip, "init-new") + elif node['value']=='work': + logger.info ("new node %s joins" % nodeip) + # setup GRE tunnels for new nodes + if self.addr == nodeip: + logger.debug ("worker start on master node. not need to setup GRE") + else: + logger.debug ("setup GRE for %s" % nodeip) + if netcontrol.gre_exists('docklet-br', nodeip): + logger.debug("GRE for %s already exists, reuse it" % nodeip) + else: + netcontrol.setup_gre('docklet-br', nodeip) + self.runnodes.append(nodeip) + self.etcd.setkey("machines/runnodes/"+nodeip, "ok") + if nodeip not in self.allnodes: + self.allnodes.append(nodeip) + self.etcd.setkey("machines/allnodes/"+nodeip, "ok") + logger.debug ("all nodes are: %s" % self.allnodes) + logger.debug ("run nodes are: %s" % self.runnodes) + self.rpcs.append(xmlrpc.client.ServerProxy("http://%s:%s" + % (nodeip, workerport))) + logger.info ("add %s:%s in rpc client list" % + (nodeip, workerport)) + + # get all run nodes' IP addr + def get_nodeips(self): + return self.allnodes + + def get_rpcs(self): + return self.rpcs + + def get_onerpc(self): + return self.rpcs[random.randint(0, len(self.rpcs)-1)] + + def rpc_to_ip(self, rpcclient): + return self.runnodes[self.rpcs.index(rpcclient)] + + def ip_to_rpc(self, nodeip): + return self.rpcs[self.runnodes.index(nodeip)] + + def get_allnodes(self): + return self.allnodes diff --git a/src/proxytool.py b/src/proxytool.py new file mode 100755 index 0000000..97e460f --- /dev/null +++ b/src/proxytool.py @@ -0,0 +1,31 @@ +#!/usr/bin/python3 + +import requests, json + +proxy_control="http://localhost:8001/api/routes" + +def get_routes(): + try: + resp = requests.get(proxy_control) + except: + return [False, 'Connect Failed'] + return [True, resp.json()] + +def set_route(path, target): + path='/'+path.strip('/') + if path=='' or target=='': + return [False, 'input not valid'] + try: + resp = requests.post(proxy_control+path, data=json.dumps({'target':target})) + except: + return [False, 'Connect Failed'] + return [True, 'set ok'] + +def delete_route(path): + path='/'+path.strip('/') + try: + resp = requests.delete(proxy_control+path) + except: + return [False, 'Connect Failed'] + # if exist and delete, status_code=204, if not exist, status_code=404 + return [True, 'delete ok'] diff --git a/src/tools.py b/src/tools.py new file mode 100755 index 0000000..b3c62d2 --- /dev/null +++ b/src/tools.py @@ -0,0 +1,23 @@ +#!/usr/bin/python3 + +import os, random + +#from log import logger + +def loadenv(configpath): + configfile = open(configpath) + #logger.info ("load environment from %s" % configpath) + for line in configfile: + line = line.strip() + if line == '': + continue + keyvalue = line.split("=") + if len(keyvalue) < 2: + continue + key = keyvalue[0].strip() + value = keyvalue[1].strip() + #logger.info ("load env and put env %s:%s" % (key, value)) + os.environ[key] = value + +def gen_token(): + return str(random.randint(10000, 99999))+"-"+str(random.randint(10000, 99999)) diff --git a/src/userManager.py b/src/userManager.py new file mode 100755 index 0000000..eaaaddd --- /dev/null +++ b/src/userManager.py @@ -0,0 +1,643 @@ +''' +userManager for Docklet +provide a class for managing users and usergroups in Docklet +Warning: in some early versions, "token" stand for the instance of class model.User + now it stands for a string that can be parsed to get that instance. + in all functions start with "@administration_required" or "@administration_or_self_required", "token" is the instance +Original author: Liu Peidong +''' + +from model import db, User, UserGroup +from functools import wraps +import os, subprocess +import hashlib +import pam +from base64 import b64encode +import env +import smtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from email.header import Header +from datetime import datetime + +email_from_address = env.getenv('EMAIL_FROM_ADDRESS') +admin_email_address = env.getenv('ADMIN_EMAIL_ADDRESS') +PAM = pam.pam() + +if (env.getenv('EXTERNAL_LOGIN').lower() == 'true'): + from plugin import external_receive + +def administration_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if ( ('cur_user' in kwargs) == False): + return {"success":'false', "reason":"Cannot get cur_user"} + cur_user = kwargs['cur_user'] + if ((cur_user.user_group == 'admin') or (cur_user.user_group == 'root')): + return func(*args, **kwargs) + else: + return {"success": 'false', "reason": 'Unauthorized Action'} + + return wrapper + +def administration_or_self_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if ( (not ('cur_user' in kwargs)) or (not ('user' in kwargs))): + return {"success":'false', "reason":"Cannot get cur_user or user"} + cur_user = kwargs['cur_user'] + user = kwargs['user'] + if ((cur_user.user_group == 'admin') or (cur_user.user_group == 'root') or (cur_user.username == user.username)): + return func(*args, **kwargs) + else: + return {"success": 'false', "reason": 'Unauthorized Action'} + + return wrapper + +def token_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if ( ('cur_user' in kwargs) == False): + return {"success":'false', "reason":"Cannot get cur_user"} + return func(*args, **kwargs) + + return wrapper + +def send_activated_email(to_address, username): + if (email_from_address in ['\'\'', '\"\"', '']): + return + #text = 'Dear '+ username + ':\n' + ' Your account in docklet has been activated' + text = '

Dear '+ username + ':

' + text += '''

      Your account in %s has been activated

+

      Enjoy your personal workspace in the cloud !

+
+

      Note: DO NOT reply to this email!

+

+

Docklet Team, SEI, PKU

+ ''' % (env.getenv("PORTAL_URL"), env.getenv("PORTAL_URL")) + text += '

'+ str(datetime.utcnow()) + '

' + text += '' + subject = 'Docklet account activated' + msg = MIMEMultipart() + textmsg = MIMEText(text,'html','utf-8') + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = email_from_address + msg['To'] = to_address + msg.attach(textmsg) + s = smtplib.SMTP() + s.connect() + s.sendmail(email_from_address, to_address, msg.as_string()) + s.close() + +def send_remind_activating_email(username): + nulladdr = ['\'\'', '\"\"', ''] + if (email_from_address in nulladdr or admin_email_address in nulladdr): + return + #text = 'Dear '+ username + ':\n' + ' Your account in docklet has been activated' + text = '

Dear '+ 'admin' + ':

' + text += '''

      An activating request for %s in %s has been sent

+

      Please check it !

+

+

Docklet Team, SEI, PKU

+ ''' % (username, env.getenv("PORTAL_URL"), env.getenv("PORTAL_URL")) + text += '

'+ str(datetime.utcnow()) + '

' + text += '' + subject = 'An activating request in Docklet has been sent' + msg = MIMEMultipart() + textmsg = MIMEText(text,'html','utf-8') + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = email_from_address + msg['To'] = admin_email_address + msg.attach(textmsg) + s = smtplib.SMTP() + s.connect() + s.sendmail(email_from_address, admin_email_address, msg.as_string()) + s.close() + + +class userManager: + def __init__(self, username = 'root', password = None): + ''' + Try to create the database when there is none + initialize 'root' user and 'root' & 'primary' group + ''' + try: + User.query.all() + UserGroup.query.all() + except: + db.create_all() + root = UserGroup('root') + db.session.add(root) + db.session.commit() + if password == None: + #set a random password + password = os.urandom(16) + password = b64encode(password).decode('utf-8') + fsdir = env.getenv('FS_PREFIX') + f = open(fsdir + '/local/generated_password.txt', 'w') + f.write("User=%s\nPass=%s\n"%(username, password)) + f.close() + sys_admin = User(username, hashlib.sha512(password.encode('utf-8')).hexdigest()) + sys_admin.status = 'normal' + sys_admin.nickname = 'root' + sys_admin.description = 'Root_User' + sys_admin.user_group = 'root' + sys_admin.auth_method = 'local' + db.session.add(sys_admin) + path = env.getenv('DOCKLET_LIB') + subprocess.call([path+"/userinit.sh", username]) + db.session.commit() + admin = UserGroup('admin') + primary = UserGroup('primary') + db.session.add(admin) + db.session.add(primary) + db.session.commit() + + def auth_local(self, username, password): + password = hashlib.sha512(password.encode('utf-8')).hexdigest() + user = User.query.filter_by(username = username).first() + if (user == None): + return {"success":'false', "reason": "User did not exist"} + if (user.password != password): + return {"success":'false', "reason": "Wrong password"} + result = { + "success": 'true', + "data":{ + "username" : user.username, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "group" : user.user_group, + "token" : user.generate_auth_token(), + } + } + return result + + def auth_pam(self, username, password): + user = User.query.filter_by(username = username).first() + pamresult = PAM.authenticate(username, password) + if (pamresult == False or (user != None and user.auth_method != 'pam')): + return {"success":'false', "reason": "Wrong password or wrong login method"} + if (user == None): + newuser = self.newuser(); + newuser.username = username + newuser.password = "no_password" + newuser.nickname = username + newuser.status = "init" + newuser.user_group = "primary" + newuser.auth_method = "pam" + self.register(user = newuser) + user = User.query.filter_by(username = username).first() + result = { + "success": 'true', + "data":{ + "username" : user.username, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "group" : user.user_group, + "token" : user.generate_auth_token(), + } + } + return result + + def auth_external(self, form): + + if (env.getenv('EXTERNAL_LOGIN') != 'True'): + failed_result = {'success': 'false', 'reason' : 'external auth disabled'} + return failed_result + + result = external_receive.external_auth_receive_request(form) + + if (result['success'] != 'True'): + failed_result = {'success':'false', 'result': result} + return failed_result + + username = result['username'] + user = User.query.filter_by(username = username).first() + if (user != None and user.auth_method == result['auth_method']): + result = { + "success": 'true', + "data":{ + "username" : user.username, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "group" : user.user_group, + "token" : user.generate_auth_token(), + } + } + return result + if (user != None and user.auth_method != result['auth_method']): + result = {'success': 'false', 'reason': 'other kinds of account already exists'} + return result + #user == None , register an account for external user + newuser = self.newuser(); + newuser.username = result['username'] + newuser.password = result['password'] + newuser.avatar = result['avatar'] + newuser.nickname = result['nickname'] + newuser.description = result['description'] + newuser.e_mail = result['e_mail'] + newuser.truename = result['truename'] + newuser.student_number = result['student_number'] + newuser.status = result['status'] + newuser.user_group = result['user_group'] + newuser.auth_method = result['auth_method'] + newuser.department = result['department'] + newuser.tel = result['tel'] + self.register(user = newuser) + user = User.query.filter_by(username = username).first() + result = { + "success": 'true', + "data":{ + "username" : user.username, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "group" : user.user_group, + "token" : user.generate_auth_token(), + } + } + return result + + def auth(self, username, password): + ''' + authenticate a user by username & password + return a token as well as some user information + ''' + user = User.query.filter_by(username = username).first() + if (user == None or user.auth_method =='pam'): + return self.auth_pam(username, password) + elif (user.auth_method == 'local'): + return self.auth_local(username, password) + else: + result = {'success':'false', 'reason':'auth_method error'} + return result + + def auth_token(self, token): + ''' + authenticate a user by a token + when succeeded, return the database iterator + otherwise return None + ''' + user = User.verify_auth_token(token) + return user + + @administration_required + def query(*args, **kwargs): + ''' + Usage: query(username = 'xxx', cur_user = token_from_auth) + || query(ID = a_integer, cur_user = token_from_auth) + Provide information about one user that administrators need to use + ''' + if ( 'ID' in kwargs): + user = User.query.filter_by(id = kwargs['ID']).first() + if (user == None): + return {"success":False, "reason":"User does not exist"} + result = { + "success":'true', + "data":{ + "username" : user.username, + "password" : user.password, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "e_mail" : user.e_mail, + "student_number": user.student_number, + "department" : user.department, + "truename" : user.truename, + "tel" : user.tel, + "register_date" : "%s"%(user.register_date), + "group" : user.user_group, + "description" : user.description, + }, + "token": user + } + return result + + if ( 'username' not in kwargs): + return {"success":'false', "reason":"Cannot get 'username'"} + username = kwargs['username'] + user = User.query.filter_by(username = username).first() + if (user == None): + return {"success":'false', "reason":"User does not exist"} + result = { + "success": 'true', + "data":{ + "username" : user.username, + "password" : user.password, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "e_mail" : user.e_mail, + "student_number": user.student_number, + "department" : user.department, + "truename" : user.truename, + "tel" : user.tel, + "register_date" : "%s"%(user.register_date), + "group" : user.user_group, + }, + "token": user + } + return result + + @token_required + def selfQuery(*args, **kwargs): + ''' + Usage: selfQuery(cur_user = token_from_auth) + List informantion for oneself + ''' + user = kwargs['cur_user'] + group = UserGroup.query.filter_by(name = user.user_group).first() + result = { + "success": 'true', + "data":{ + "username" : user.username, + "password" : user.password, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "e_mail" : user.e_mail, + "student_number": user.student_number, + "department" : user.department, + "truename" : user.truename, + "tel" : user.tel, + "register_date" : "%s"%(user.register_date), + "group" : user.user_group, + "groupinfo": { + "cpu": group.cpu, + "memory": group.memory, + "imageQuantity": group.imageQuantity, + "lifeCycle":group.lifeCycle, + }, + }, + } + return result + + @token_required + def selfModify(*args, **kwargs): + ''' + Usage: selfModify(cur_user = token_from_auth, newValue = form) + Modify informantion for oneself + ''' + form = kwargs['newValue'] + name = form.getvalue('name', None) + value = form.getvalue('value', None) + if (name == None or value == None): + result = {'success': 'false'} + return result + user = User.query.filter_by(username = kwargs['cur_user'].username).first() + if (name == 'nickname'): + user.nickname = value + elif (name == 'description'): + user.description = value + elif (name == 'department'): + user.department = value + elif (name == 'e_mail'): + user.e_mail = value + elif (name == 'tel'): + user.tel = value + else: + result = {'success': 'false'} + return result + db.session.commit() + result = {'success': 'true'} + return result + + + @administration_required + def userList(*args, **kwargs): + ''' + Usage: list(cur_user = token_from_auth) + List all users for an administrator + ''' + alluser = User.query.all() + result = { + "success": 'true', + "data":[] + } + for user in alluser: + userinfo = [ + user.id, + user.username, + user.truename, + user.e_mail, + user.tel, + "%s"%(user.register_date), + user.status, + user.user_group, + '', + ] + result["data"].append(userinfo) + return result + + @administration_required + def groupList(*args, **kwargs): + ''' + Usage: list(cur_user = token_from_auth) + List all groups for an administrator + ''' + allgroup = UserGroup.query.all() + result = { + "success": 'true', + "data":[] + } + for group in allgroup: + groupinfo = [ + group.id, + group.name, + group.cpu, + group.memory, + group.imageQuantity, + group.lifeCycle, + '', + ] + result["data"].append(groupinfo) + return result + + @administration_required + def groupQuery(*args, **kwargs): + ''' + Usage: groupQuery(id = XXX, cur_user = token_from_auth) + List a group for an administrator + ''' + group = UserGroup.query.filter_by(id = kwargs['ID']).first() + if (group == None): + return {"success":False, "reason":"Group does not exist"} + result = { + "success":'true', + "data":{ + "name" : group.name , + "cpu" : group.cpu , + "memory" : group.memory, + "imageQuantity" : group.imageQuantity, + "lifeCycle" : group.lifeCycle, + } + } + return result + + @administration_required + def groupListName(*args, **kwargs): + ''' + Usage: grouplist(cur_user = token_from_auth) + List all group names for an administrator + ''' + groups = UserGroup.query.all() + result = { + "groups": [], + } + for group in groups: + result["groups"].append(group.name) + return result + + @administration_required + def groupModify(*args, **kwargs): + ''' + Usage: groupModify(newValue = dict_from_form, cur_user = token_from_auth) + ''' + group_modify = UserGroup.query.filter_by(name = kwargs['newValue'].getvalue('groupname', None)).first() + if (group_modify == None): + return {"success":'false', "reason":"UserGroup does not exist"} + form = kwargs['newValue'] + group_modify.cpu = form.getvalue('cpu', '') + group_modify.memory = form.getvalue('memory', '') + group_modify.imageQuantity = form.getvalue('image', '') + group_modify.lifeCycle = form.getvalue('lifecycle', '') + db.session.commit() + return {"success":'true'} + + @administration_required + def modify(*args, **kwargs): + ''' + modify a user's information in database + will send an e-mail when status is changed from 'applying' to 'normal' + Usage: modify(newValue = dict_from_form, cur_user = token_from_auth) + ''' + user_modify = User.query.filter_by(username = kwargs['newValue'].getvalue('username', None)).first() + if (user_modify == None): + + return {"success":'false', "reason":"User does not exist"} + + #try: + form = kwargs['newValue'] + user_modify.truename = form.getvalue('truename', '') + user_modify.e_mail = form.getvalue('e_mail', '') + user_modify.department = form.getvalue('department', '') + user_modify.student_number = form.getvalue('student_number', '') + user_modify.tel = form.getvalue('tel', '') + user_modify.user_group = form.getvalue('group', '') + user_modify.auth_method = form.getvalue('auth_method', '') + if (user_modify.status == 'applying' and form.getvalue('status', '') == 'normal'): + send_activated_email(user_modify.e_mail, user_modify.username) + user_modify.status = form.getvalue('status', '') + if (form.getvalue('Chpassword', '') == 'Yes'): + new_password = form.getvalue('password','no_password') + new_password = hashlib.sha512(new_password.encode('utf-8')).hexdigest() + user_modify.password = new_password + #self.chpassword(cur_user = user_modify, password = form.getvalue('password','no_password')) + + db.session.commit() + return {"success":'true'} + #except: + #return {"success":'false', "reason":"Something happened"} + + @token_required + def chpassword(*args, **kwargs): + ''' + Usage: chpassword(cur_user = token_from_auth, password = 'your_password') + ''' + cur_user = kwargs['cur_user'] + cur_user.password = hashlib.sha512(kwargs['password'].encode('utf-8')).hexdigest() + + def newuser(*args, **kwargs): + ''' + Usage : newuser() + The only method to create a new user + call this method first, modify the return value which is a database row instance,then call self.register() + ''' + user_new = User('newuser', 'asdf1234') + user_new.user_group = 'primary' + user_new.avatar = 'default.png' + return user_new + + def register(*args, **kwargs): + ''' + Usage: register(user = modified_from_newuser()) + ''' + + if (kwargs['user'].username == None or kwargs['user'].username == ''): + return {"success":'false', "reason": "Empty username"} + user_check = User.query.filter_by(username = kwargs['user'].username).first() + if (user_check != None and user_check.status != "init"): + #for the activating form + return {"success":'false', "reason": "Unauthorized action"} + if (user_check != None and (user_check.status == "init")): + db.session.delete(user_check) + db.session.commit() + newuser = kwargs['user'] + newuser.password = hashlib.sha512(newuser.password.encode('utf-8')).hexdigest() + db.session.add(newuser) + db.session.commit() + + # if newuser status is normal, init some data for this user + # now initialize for all kind of users + #if newuser.status == 'normal': + path = env.getenv('DOCKLET_LIB') + subprocess.call([path+"/userinit.sh", newuser.username]) + return {"success":'true'} + + @administration_required + def groupadd(*args, **kwargs): + name = kwargs.get('name', None) + if (name == None): + return {"success":'false', "reason": "Empty group name"} + group_new = UserGroup(name) + db.session.add(group_new) + db.session.commit() + return {"success":'true'} + + def queryForDisplay(*args, **kwargs): + ''' + Usage: queryForDisplay(user = token_from_auth) + Provide information about one user that administrators need to use + ''' + + if ( 'user' not in kwargs): + return {"success":'false', "reason":"Cannot get 'user'"} + user = kwargs['user'] + if (user == None): + return {"success":'false', "reason":"User does not exist"} + result = { + "success": 'true', + "data":{ + "username" : user.username, + "password" : user.password, + "avatar" : user.avatar, + "nickname" : user.nickname, + "description" : user.description, + "status" : user.status, + "e_mail" : user.e_mail, + "student_number": user.student_number, + "department" : user.department, + "truename" : user.truename, + "tel" : user.tel, + "register_date" : "%s"%(user.register_date), + "group" : user.user_group, + "auth_method": user.auth_method, + } + } + return result + +# def usermodify(rowID, columnID, newValue, cur_user): +# '''not used now''' +# user = um.query(ID = request.form["rowID"], cur_user = root).get('token', None) +# result = um.modify(user = user, columnID = request.form["columnID"], newValue = request.form["newValue"], cur_user = root) +# return json.dumps(result) diff --git a/src/userinit.sh b/src/userinit.sh new file mode 100755 index 0000000..3ae8e0f --- /dev/null +++ b/src/userinit.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# initialize for a new user +# initialize directory : clusters, data, ssh +# generate ssh keys for new user + +[ -z $FS_PREFIX ] && FS_PREFIX="/opt/docklet" + +USERNAME=$1 + +[ -z $USERNAME ] && echo "[userinit.sh] USERNAME is needed" && exit 1 + +echo "[Info] [userinit.sh] initialize for user $USERNAME" + +USER_DIR=$FS_PREFIX/global/users/$USERNAME +[ -d $USER_DIR ] && echo "[userinit.sh] user directory already exists, delete it" && rm -r $USER_DIR + +mkdir -p $USER_DIR/{clusters,hosts,data,ssh} + +SSH_DIR=$USER_DIR/ssh +# here generate id_rsa.pub has "user@hostname" at the end +# maybe it should be delete +ssh-keygen -t rsa -P '' -f $SSH_DIR/id_rsa &>/dev/null +cp $SSH_DIR/id_rsa.pub $SSH_DIR/authorized_keys diff --git a/src/vclustermgr.py b/src/vclustermgr.py new file mode 100755 index 0000000..c09441b --- /dev/null +++ b/src/vclustermgr.py @@ -0,0 +1,397 @@ +#!/usr/bin/python3 + +import os, random, json, sys, imagemgr +import datetime + +from log import logger +import env +import proxytool + +################################################## +# VclusterMgr +# Description : VclusterMgr start/stop/manage virtual clusters +# +################################################## + +class VclusterMgr(object): + def __init__(self, nodemgr, networkmgr, etcdclient, addr, mode): + self.mode = mode + self.nodemgr = nodemgr + self.imgmgr = imagemgr.ImageMgr() + self.networkmgr = networkmgr + self.addr = addr + self.etcd = etcdclient + self.defaultsize = env.getenv("CLUSTER_SIZE") + self.fspath = env.getenv("FS_PREFIX") + + logger.info ("vcluster start on %s" % (self.addr)) + if self.mode == 'new': + logger.info ("starting in new mode on %s" % (self.addr)) + # check if all clusters data are deleted in httprest.py + clean = True + usersdir = self.fspath+"/global/users/" + for user in os.listdir(usersdir): + if len(os.listdir(usersdir+user+"/clusters")) > 0 or len(os.listdir(usersdir+user+"/hosts")) > 0: + clean = False + if not clean: + logger.error ("clusters files not clean, start failed") + sys.exit(1) + elif self.mode == "recovery": + logger.info ("starting in recovery mode on %s" % (self.addr)) + self.recover_allclusters() + else: + logger.error ("not supported mode:%s" % self.mode) + sys.exit(1) + + def recover_allclusters(self): + logger.info("recovering all vclusters for all users...") + usersdir = self.fspath+"/global/users/" + for user in os.listdir(usersdir): + for cluster in self.list_clusters(user)[1]: + logger.info ("recovering cluster:%s for user:%s ..." % (cluster, user)) + self.recover_cluster(cluster, user) + logger.info("recovered all vclusters for all users") + + def create_cluster(self, clustername, username, image, user_info): + if self.is_cluster(clustername, username): + return [False, "cluster:%s already exists" % clustername] + clustersize = int(self.defaultsize); + logger.info ("starting cluster %s with %d containers for %s" % (clustername, int(clustersize), username)) + workers = self.nodemgr.get_rpcs() + image_json = json.dumps(image) + if (len(workers) == 0): + logger.warning ("no workers to start containers, start cluster failed") + return [False, "no workers are running"] + # check user IP pool status, should be moved to user init later + if not self.networkmgr.has_user(username): + self.networkmgr.add_user(username, cidr=29) + [status, result] = self.networkmgr.acquire_userips_cidr(username, clustersize) + gateway = self.networkmgr.get_usergw(username) + vlanid = self.networkmgr.get_uservlanid(username) + logger.info ("create cluster with gateway : %s" % gateway) + self.networkmgr.printpools() + if not status: + logger.info ("create cluster failed: %s" % result) + return [False, result] + ips = result + clusterid = self._acquire_id() + clusterpath = self.fspath+"/global/users/"+username+"/clusters/"+clustername + hostpath = self.fspath+"/global/users/"+username+"/hosts/"+str(clusterid)+".hosts" + hosts = "127.0.0.1\tlocalhost\n" + containers = [] + for i in range(0, clustersize): + onework = workers[random.randint(0, len(workers)-1)] + lxc_name = username + "-" + str(clusterid) + "-" + str(i) + hostname = "host-"+str(i) + logger.info ("create container with : name-%s, username-%s, clustername-%s, clusterid-%s, hostname-%s, ip-%s, gateway-%s, image-%s" % (lxc_name, username, clustername, str(clusterid), hostname, ips[i], gateway, image_json)) + [success,message] = onework.create_container(lxc_name, username, user_info , clustername, str(clusterid), hostname, ips[i], gateway, str(vlanid), image_json) + if success is False: + logger.info("container create failed, so vcluster create failed") + return [False, message] + logger.info("container create success") + hosts = hosts + ips[i].split("/")[0] + "\t" + hostname + "\t" + hostname + "."+clustername + "\n" + containers.append({ 'containername':lxc_name, 'hostname':hostname, 'ip':ips[i], 'host':self.nodemgr.rpc_to_ip(onework), 'image':image['name'], 'lastsave':datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) + hostfile = open(hostpath, 'w') + hostfile.write(hosts) + hostfile.close() + clusterfile = open(clusterpath, 'w') + proxy_url = env.getenv("PORTAL_URL") + "/_web/" + username + "/" + clustername + info = {'clusterid':clusterid, 'status':'stopped', 'size':clustersize, 'containers':containers, 'nextcid': clustersize, 'create_time':datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 'start_time':"------" , 'proxy_url':proxy_url} + clusterfile.write(json.dumps(info)) + clusterfile.close() + return [True, info] + + def scale_out_cluster(self,clustername,username,image,user_info): + if not self.is_cluster(clustername,username): + return [False, "cluster:%s not found" % clustername] + workers = self.nodemgr.get_rpcs() + if (len(workers) == 0): + logger.warning("no workers to start containers, scale out failed") + return [False, "no workers are running"] + image_json = json.dumps(image) + [status, result] = self.networkmgr.acquire_userips_cidr(username) + gateway = self.networkmgr.get_usergw(username) + vlanid = self.networkmgr.get_uservlanid(username) + self.networkmgr.printpools() + if not status: + return [False, result] + ip = result[0] + [status, clusterinfo] = self.get_clusterinfo(clustername,username) + clusterid = clusterinfo['clusterid'] + clusterpath = self.fspath + "/global/users/" + username + "/clusters/" + clustername + hostpath = self.fspath + "/global/users/" + username + "/hosts/" + str(clusterid) + ".hosts" + cid = clusterinfo['nextcid'] + onework = workers[random.randint(0, len(workers)-1)] + lxc_name = username + "-" + str(clusterid) + "-" + str(cid) + hostname = "host-" + str(cid) + [success, message] = onework.create_container(lxc_name, username, user_info, clustername, clusterid, hostname, ip, gateway, str(vlanid), image_json) + if success is False: + logger.info("create container failed, so scale out failed") + return [False, message] + if clusterinfo['status'] == "running": + onework.start_container(lxc_name) + onework.start_services(lxc_name, ["ssh"]) # TODO: need fix + logger.info("scale out success") + hostfile = open(hostpath, 'a') + hostfile.write(ip.split("/")[0] + "\t" + hostname + "\t" + hostname + "." + clustername + "\n") + hostfile.close() + clusterinfo['nextcid'] = int(clusterinfo['nextcid']) + 1 + clusterinfo['size'] = int(clusterinfo['size']) + 1 + clusterinfo['containers'].append({'containername':lxc_name, 'hostname':hostname, 'ip':ip, 'host':self.nodemgr.rpc_to_ip(onework), 'image':image['name'], 'lastsave':datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") }) + clusterfile = open(clusterpath, 'w') + clusterfile.write(json.dumps(clusterinfo)) + clusterfile.close() + return [True, clusterinfo] + + def addproxy(self,username,clustername,ip,port): + [status, clusterinfo] = self.get_clusterinfo(clustername, username) + if 'proxy_ip' in clusterinfo: + return [False, "proxy already exists"] + target = "http://" + ip + ":" + port + clusterinfo['proxy_ip'] = ip + ":" + port + clusterfile = open(self.fspath + "/global/users/" + username + "/clusters/" + clustername, 'w') + clusterfile.write(json.dumps(clusterinfo)) + clusterfile.close() + proxytool.set_route("/_web/" + username + "/" + clustername, target) + return [True, clusterinfo] + + def deleteproxy(self, username, clustername): + [status, clusterinfo] = self.get_clusterinfo(clustername, username) + if 'proxy_ip' not in clusterinfo: + return [False, "proxy not exists"] + clusterinfo.pop('proxy_ip') + clusterfile = open(self.fspath + "/global/users/" + username + "/clusters/" + clustername, 'w') + clusterfile.write(json.dumps(clusterinfo)) + clusterfile.close() + proxytool.delete_route("/_web/" + username + "/" + clustername) + return [True, clusterinfo] + + def flush_cluster(self,username,clustername,containername): + begintime = datetime.datetime.now() + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + containers = info['containers'] + imagetmp = username + "_tmp_docklet" + for container in containers: + if container['containername'] == containername: + logger.info("container: %s found" % containername) + onework = self.nodemgr.ip_to_rpc(container['host']) + onework.create_image(username,imagetmp,containername) + fimage = container['image'] + logger.info("image: %s created" % imagetmp) + break + else: + logger.error("container: %s not found" % containername) + for container in containers: + if container['containername'] != containername: + logger.info("container: %s now flush" % container['containername']) + onework = self.nodemgr.ip_to_rpc(container['host']) + #t = threading.Thread(target=onework.flush_container,args=(username,imagetmp,container['containername'])) + #threads.append(t) + onework.flush_container(username,imagetmp,container['containername']) + container['lastsave'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + container['image'] = fimage + logger.info("thread for container: %s has been prepared" % container['containername']) + clusterpath = self.fspath + "/global/users/" + username + "/clusters/" + clustername + infofile = open(clusterpath,'w') + infofile.write(json.dumps(info)) + infofile.close() + self.imgmgr.removeImage(username,imagetmp) + endtime = datetime.datetime.now() + dtime = (endtime - begintime).seconds + logger.info("flush spend %s seconds" % dtime) + logger.info("flush success") + + + def create_image(self,username,clustername,containername,imagename,description,isforce=False): + [status, info] = self.get_clusterinfo(clustername,username) + if not status: + return [False, "cluster not found"] + containers = info['containers'] + for container in containers: + if container['containername'] == containername: + logger.info("container: %s found" % containername) + onework = self.nodemgr.ip_to_rpc(container['host']) + res = onework.create_image(username,imagename,containername,description,isforce) + container['lastsave'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + container['image'] = imagename + break + else: + res = [False, "container not found"] + logger.error("container: %s not found" % containername) + clusterpath = self.fspath + "/global/users/" + username + "/clusters/" + clustername + infofile = open(clusterpath, 'w') + infofile.write(json.dumps(info)) + infofile.close() + return res + + def delete_cluster(self, clustername, username): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + if info['status']=='running': + return [False, "cluster is still running, you need to stop it and then delete"] + ips = [] + for container in info['containers']: + worker = self.nodemgr.ip_to_rpc(container['host']) + worker.delete_container(container['containername']) + ips.append(container['ip']) + logger.info("delete vcluster and release vcluster ips") + self.networkmgr.release_userips(username, ips) + self.networkmgr.printpools() + os.remove(self.fspath+"/global/users/"+username+"/clusters/"+clustername) + os.remove(self.fspath+"/global/users/"+username+"/hosts/"+str(info['clusterid'])+".hosts") + return [True, "cluster delete"] + + def scale_in_cluster(self, clustername, username, containername): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + new_containers = [] + for container in info['containers']: + if container['containername'] == containername: + worker = self.nodemgr.ip_to_rpc(container['host']) + worker.delete_container(containername) + self.networkmgr.release_userips(username, container['ip']) + self.networkmgr.printpools() + else: + new_containers.append(container) + info['containers'] = new_containers + info['size'] -= 1 + cid = containername[containername.rindex("-")+1:] + clusterid = info['clusterid'] + clusterpath = self.fspath + "/global/users/" + username + "/clusters/" + clustername + hostpath = self.fspath + "/global/users/" + username + "/hosts/" + str(clusterid) + ".hosts" + clusterfile = open(clusterpath, 'w') + clusterfile.write(json.dumps(info)) + clusterfile.close() + hostfile = open(hostpath, 'r') + hostinfo = hostfile.readlines() + hostfile.close() + hostfile = open(hostpath, 'w') + new_hostinfo = [] + new_hostinfo.append(hostinfo[0]) + for host in hostinfo[1:]: + parts = host.split("\t") + if parts[1][parts[1].rindex("-")+1:] == cid: + pass + else: + new_hostinfo.append(host) + hostfile.writelines(new_hostinfo) + hostfile.close() + return [True, info] + + + def start_cluster(self, clustername, username): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + if info['status'] == 'running': + return [False, "cluster is already running"] + # check gateway for user + # after reboot, user gateway goes down and lose its configuration + # so, check is necessary + self.networkmgr.check_usergw(username) + # set proxy + try: + target = 'http://'+info['containers'][0]['ip'].split('/')[0]+":10000" + proxytool.set_route('/go/'+username+'/'+clustername, target) + except: + return [False, "start cluster failed with setting proxy failed"] + for container in info['containers']: + worker = self.nodemgr.ip_to_rpc(container['host']) + worker.start_container(container['containername']) + worker.start_services(container['containername']) + info['status']='running' + info['start_time']=datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + infofile = open(self.fspath+"/global/users/"+username+"/clusters/"+clustername, 'w') + infofile.write(json.dumps(info)) + infofile.close() + return [True, "start cluster"] + + def recover_cluster(self, clustername, username): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + if info['status'] == 'stopped': + return [True, "cluster no need to start"] + # need to check and recover gateway of this user + self.networkmgr.check_usergw(username) + # recover proxy of cluster + try: + target = 'http://'+info['containers'][0]['ip'].split('/')[0]+":10000" + proxytool.set_route('/go/'+username+'/'+clustername, target) + except: + return [False, "start cluster failed with setting proxy failed"] + info['containers'][0] + # recover containers of this cluster + for container in info['containers']: + worker = self.nodemgr.ip_to_rpc(container['host']) + worker.recover_container(container['containername']) + return [True, "start cluster"] + + + # maybe here should use cluster id + def stop_cluster(self, clustername, username): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return [False, "cluster not found"] + if info['status'] == 'stopped': + return [False, 'cluster is already stopped'] + for container in info['containers']: + worker = self.nodemgr.ip_to_rpc(container['host']) + worker.stop_container(container['containername']) + info['status']='stopped' + info['start_time']="------" + infofile = open(self.fspath+"/global/users/"+username+"/clusters/"+clustername, 'w') + infofile.write(json.dumps(info)) + infofile.close() + return [True, "start cluster"] + + def list_clusters(self, user): + if not os.path.exists(self.fspath+"/global/users/"+user+"/clusters"): + return [True, []] + clusters = os.listdir(self.fspath+"/global/users/"+user+"/clusters") + full_clusters = [] + for cluster in clusters: + single_cluster = {} + single_cluster['name'] = cluster + [status, info] = self.get_clusterinfo(cluster,user) + if info['status'] == 'running': + single_cluster['status'] = 'running' + else: + single_cluster['status'] = 'stopping' + full_clusters.append(single_cluster) + return [True, clusters] + + def is_cluster(self, clustername, username): + [status, clusters] = self.list_clusters(username) + if clustername in clusters: + return True + else: + return False + + # get id from name + def get_clusterid(self, clustername, username): + [status, info] = self.get_clusterinfo(clustername, username) + if not status: + return -1 + if 'clusterid' in info: + return int(info['clusterid']) + logger.error ("internal error: cluster:%s info file has no clusterid " % clustername) + return -1 + + def get_clusterinfo(self, clustername, username): + clusterpath = self.fspath + "/global/users/" + username + "/clusters/" + clustername + if not os.path.isfile(clusterpath): + return [False, "cluster not found"] + infofile = open(clusterpath, 'r') + info = json.loads(infofile.read()) + return [True, info] + + # acquire cluster id from etcd + def _acquire_id(self): + clusterid = self.etcd.getkey("vcluster/nextid")[1] + self.etcd.setkey("vcluster/nextid", str(int(clusterid)+1)) + return int(clusterid) diff --git a/src/worker.py b/src/worker.py new file mode 100755 index 0000000..f3e9608 --- /dev/null +++ b/src/worker.py @@ -0,0 +1,201 @@ +#!/usr/bin/python3 + +# first init env +import env, tools +config = env.getenv("CONFIG") +tools.loadenv(config) + +# must import logger after initlogging, ugly +from log import initlogging +initlogging("docklet-worker") +from log import logger + +import xmlrpc.server, sys, time +from socketserver import ThreadingMixIn +import etcdlib, network, container +from nettools import netcontrol +import monitor +from lvmtool import * + +################################################################## +# Worker +# Description : Worker starts at worker node to listen rpc request and complete the work +# Init() : +# get master ip +# initialize rpc server +# register rpc functions +# initialize network +# initialize lvm group +# Start() : +# register in etcd +# setup GRE tunnel +# start rpc service +################################################################## + +class ThreadXMLRPCServer(ThreadingMixIn,xmlrpc.server.SimpleXMLRPCServer): + pass + +class Worker(object): + def __init__(self, etcdclient, addr, port): + self.addr = addr + self.port = port + logger.info ("begin initialize on %s" % self.addr) + + self.fspath = env.getenv('FS_PREFIX') + self.poolsize = env.getenv('DISKPOOL_SIZE') + + self.etcd = etcdclient + self.master = self.etcd.getkey("service/master")[1] + self.mode=None + + # register self to master + self.etcd.setkey("machines/runnodes/"+self.addr, "waiting") + for f in range (0, 3): + [status, value] = self.etcd.getkey("machines/runnodes/"+self.addr) + if not value.startswith("init"): + # master wakesup every 0.1s to check register + logger.debug("worker % register to master failed %d \ + time, sleep %fs" % (self.addr, f+1, 0.1)) + time.sleep(0.1) + else: + break + + if value.startswith("init"): + # check token to check global directory + [status, token_1] = self.etcd.getkey("token") + tokenfile = open(self.fspath+"/global/token", 'r') + token_2 = tokenfile.readline().strip() + if token_1 != token_2: + logger.error("check token failed, global directory is not a shared filesystem") + sys.exit(1) + else: + logger.error ("worker register in machines/runnodes failed, maybe master not start") + sys.exit(1) + logger.info ("worker registered in master and checked the token") + + Containers = container.Container(self.addr, etcdclient) + if value == 'init-new': + logger.info ("init worker with mode:new") + self.mode='new' + # check global directory do not have containers on this worker + [both, onlylocal, onlyglobal] = Containers.diff_containers() + if len(both+onlyglobal) > 0: + logger.error ("mode:new will clean containers recorded in global, please check") + sys.exit(1) + [status, info] = Containers.delete_allcontainers() + if not status: + logger.error ("delete all containers failed") + sys.exit(1) + # create new lvm VG at last + new_group("docklet-group",self.poolsize,self.fspath+"/local/docklet-storage") + #subprocess.call([self.libpath+"/lvmtool.sh", "new", "group", "docklet-group", self.poolsize, self.fspath+"/local/docklet-storage"]) + elif value == 'init-recovery': + logger.info ("init worker with mode:recovery") + self.mode='recovery' + # recover lvm VG first + recover_group("docklet-group",self.fspath+"/local/docklet-storage") + #subprocess.call([self.libpath+"/lvmtool.sh", "recover", "group", "docklet-group", self.fspath+"/local/docklet-storage"]) + [status, meg] = Containers.check_allcontainers() + if status: + logger.info ("all containers check ok") + else: + logger.info ("not all containers check ok") + #sys.exit(1) + else: + logger.error ("worker init mode:%s not supported" % value) + sys.exit(1) + # initialize rpc + # xmlrpc.server.SimpleXMLRPCServer(addr) -- addr : (ip-addr, port) + # if ip-addr is "", it will listen ports of all IPs of this host + logger.info ("initialize rpcserver %s:%d" % (self.addr, int(self.port))) + # logRequests=False : not print rpc log + #self.rpcserver = xmlrpc.server.SimpleXMLRPCServer((self.addr, self.port), logRequests=False) + self.rpcserver = ThreadXMLRPCServer((self.addr, int(self.port)), allow_none=True) + self.rpcserver.register_introspection_functions() + self.rpcserver.register_instance(Containers) + # register functions or instances to server for rpc + #self.rpcserver.register_function(function_name) + + # initialize the network + # if worker and master run on the same node, reuse bridges + # don't need to create new bridges + if (self.addr == self.master): + logger.info ("master also on this node. reuse master's network") + else: + logger.info ("initialize network") + # 'docklet-br' of worker do not need IP Addr. + #[status, result] = self.etcd.getkey("network/workbridge") + #if not status: + # logger.error ("get bridge IP failed, please check whether master set bridge IP for worker") + #self.bridgeip = result + # create bridges for worker + #network.netsetup("init", self.bridgeip) + if self.mode == 'new': + if netcontrol.bridge_exists('docklet-br'): + netcontrol.del_bridge('docklet-br') + netcontrol.new_bridge('docklet-br') + else: + if not netcontrol.bridge_exists('docklet-br'): + logger.error("docklet-br not found") + sys.exit(1) + logger.info ("setup GRE tunnel to master %s" % self.master) + #network.netsetup("gre", self.master) + if not netcontrol.gre_exists('docklet-br', self.master): + netcontrol.setup_gre('docklet-br', self.master) + + # start service of worker + def start(self): + self.etcd.setkey("machines/runnodes/"+self.addr, "work") + # start serving for rpc + logger.info ("begins to work") + self.rpcserver.serve_forever() + + +if __name__ == '__main__': + + etcdaddr = env.getenv("ETCD") + logger.info ("using ETCD %s" % etcdaddr ) + + clustername = env.getenv("CLUSTER_NAME") + logger.info ("using CLUSTER_NAME %s" % clustername ) + + # get network interface + net_dev = env.getenv("NETWORK_DEVICE") + logger.info ("using NETWORK_DEVICE %s" % net_dev ) + + ipaddr = network.getip(net_dev) + if ipaddr is False: + logger.error("network device is not correct") + sys.exit(1) + else: + logger.info("using ipaddr %s" % ipaddr) + # init etcdlib client + try: + etcdclient = etcdlib.Client(etcdaddr, prefix = clustername) + except Exception: + logger.error ("connect etcd failed, maybe etcd address not correct...") + sys.exit(1) + else: + logger.info("etcd connected") + + # init collector to collect monitor infomation + collector = monitor.Collector(etcdaddr,clustername,ipaddr) + collector.start() + + cpu_quota = env.getenv('CONTAINER_CPU') + logger.info ("using CONTAINER_CPU %s" % cpu_quota ) + + mem_quota = env.getenv('CONTAINER_MEMORY') + logger.info ("using CONTAINER_MEMORY %s" % mem_quota ) + + worker_port = env.getenv('WORKER_PORT') + logger.info ("using WORKER_PORT %s" % worker_port ) + + con_collector = monitor.Container_Collector(etcdaddr, clustername, + ipaddr, cpu_quota, mem_quota) + con_collector.start() + logger.info("CPU and Memory usage monitor started") + + logger.info("Starting worker") + worker = Worker(etcdclient, addr=ipaddr, port=worker_port) + worker.start() diff --git a/tools/DOCKLET_NOTES.txt b/tools/DOCKLET_NOTES.txt new file mode 100644 index 0000000..e03db53 --- /dev/null +++ b/tools/DOCKLET_NOTES.txt @@ -0,0 +1,17 @@ +** MUST READ ** + +1. Please keep your important data in ~/nfs directory. It will not be +destroyed even if the workspace is deleted. + +2. If you delete your workspace, all data in your Home directory will +be lost, except those in ~/nfs directory. + +3. You can save your workspace as a private image if you have modified +the system and do not want to repeat it in your new workspace or new +container. + +4. Your containers are distributed by default. So it is ideal for simple +parallel jobs. + +5. If you find the Web Terminal not align correctly, choose a monospace +font may help. diff --git a/tools/etcd-multi-nodes.sh b/tools/etcd-multi-nodes.sh new file mode 100755 index 0000000..db94120 --- /dev/null +++ b/tools/etcd-multi-nodes.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# more details for https://coreos.com/etcd/docs/latest + +which etcd &>/dev/null || { echo "etcd not installed, please install etcd first" && exit 1; } + +if [ $# -eq 0 ] ; then + echo "Usage: `basename $0` ip1 ip2 ip3" + echo " ip1 ip2 ip3 are the ip address of node etcd_1 etcd_2 etcd_3" + exit 1 +fi + +index=1 +while [ $# -gt 0 ] ; do + h="etcd_$index" + if [ $index -eq 1 ] ; then + CLUSTER="$h=http://$1:2380" + else + CLUSTER="$CLUSTER,$h=http://$1:2380" + fi + index=$(($index+1)) + shift +done + +# -initial-advertise-peer-urls : tell others what peer urls of me +# -listen-peer-urls : what peer urls of me + +# -listen-client-urls : what client urls to listen +# -advertise-client-urls : tell others what client urls to listen of me + +# -initial-cluster-state : new means join a new cluster; existing means join an existing cluster +# : new not means clear + + +etcd --name etcd_1 \ + --initial-advertise-peer-urls http://$etcd_1:2380 \ + --listen-peer-urls http://$etcd_1:2380 \ + --listen-client-urls http://$etcd_1:2379 \ + --advertise-client-urls http://$etcd_1:2379 \ + --initial-cluster-token etcd-cluster \ + --initial-cluster $CLUSTER \ + --initial-cluster-state new diff --git a/tools/etcd-one-node.sh b/tools/etcd-one-node.sh new file mode 100755 index 0000000..7192dd6 --- /dev/null +++ b/tools/etcd-one-node.sh @@ -0,0 +1,45 @@ +#!/bin/sh + +# more details for https://coreos.com/etcd/docs/latest + +#which etcd &>/dev/null || { echo "etcd not installed, please install etcd first" && exit 1; } +which etcd >/dev/null || { echo "etcd not installed, please install etcd first" && exit 1; } + +etcd_1=localhost + +if [ $# -gt 0 ] ; then + etcd_1=$1 +fi + + +# -initial-advertise-peer-urls : tell others what peer urls of me +# -listen-peer-urls : what peer urls of me + +# -listen-client-urls : what client urls to listen +# -advertise-client-urls : tell others what client urls to listen of me + +# -initial-cluster-state : new means join a new cluster; existing means a new node join an existing cluster +# : new not means clear, old data is still alive + +depdir=${0%/*} +tempdir=/opt/docklet/local +[ ! -d $tempdir/log ] && mkdir -p $tempdir/log +[ ! -d $tempdir/run ] && mkdir -p $tempdir/run + +echo "starting etcd on $etcd_1" + +#stdbuf -o0 -e0 $tempdir/etcd --name etcd_1 \ +etcd --name etcd_1 \ + --data-dir $tempdir/etcd_data \ + --initial-advertise-peer-urls http://$etcd_1:2380 \ + --listen-peer-urls http://$etcd_1:2380 \ + --listen-client-urls http://$etcd_1:2379 \ + --advertise-client-urls http://$etcd_1:2379 \ + --initial-cluster-token etcd_cluster \ + --initial-cluster etcd_1=http://$etcd_1:2380 \ + --initial-cluster-state new > $tempdir/log/etcd.log 2>&1 & + +etcdpid=$! +echo "etcd start with pid: $etcdpid and log:$tempdir/log/etcd.log" +echo $etcdpid > $tempdir/run/etcd.pid + diff --git a/tools/npmrc b/tools/npmrc new file mode 100644 index 0000000..3bd4c82 --- /dev/null +++ b/tools/npmrc @@ -0,0 +1 @@ +registry = https://registry.npm.taobao.org diff --git a/tools/pip.conf b/tools/pip.conf new file mode 100644 index 0000000..4026cd4 --- /dev/null +++ b/tools/pip.conf @@ -0,0 +1,2 @@ +[global] +index-url=https://pypi.mirrors.ustc.edu.cn/simple/ diff --git a/tools/resolv.conf b/tools/resolv.conf new file mode 100644 index 0000000..c7271f2 --- /dev/null +++ b/tools/resolv.conf @@ -0,0 +1,2 @@ +nameserver 162.105.129.26 +nameserver 162.105.129.27 diff --git a/tools/sources.list b/tools/sources.list new file mode 100644 index 0000000..8b31aad --- /dev/null +++ b/tools/sources.list @@ -0,0 +1 @@ +deb https://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse diff --git a/tools/start_jupyter.sh b/tools/start_jupyter.sh new file mode 100755 index 0000000..73a5976 --- /dev/null +++ b/tools/start_jupyter.sh @@ -0,0 +1,56 @@ +#!/bin/sh + +# +# this script should be placed in basefs/home/jupyter +# + +# This next line determines what user the script runs as. +DAEMON_USER=root + +# settings for docklet worker +DAEMON=/usr/local/bin/jupyterhub-singleuser +DAEMON_NAME=jupyter +# The process ID of the script when it runs is stored here: +PIDFILE=/home/jupyter/$DAEMON_NAME.pid + +RUN_DIR=/root + +#export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games + +#export HOME=/home + +#export SHELL=/bin/bash + +#export LOGNAME=root + +# JPY_API_TOKEN is needed by jupyterhub-singleuser +# it will send this token in request header to hub-api-url for authorization +# but we don't use this by now +export JPY_API_TOKEN=not-use + +# user for this notebook +USER=root +# port to start service +PORT=10000 +# cookie name to get from http request and send to hub_api_url for authorization +COOKIE_NAME=docklet-jupyter-cookie +# base url of this server. client will use this url for request +BASE_URL=/workspace/$USER +# prefix for login and logout +HUB_PREFIX=/jupyter +# URL for authorising cookie +HUB_API_URL=http://192.168.192.64:9000/jupyter +# IP for listening request +IP=0.0.0.0 + +[ -f /home/jupyter/jupyter.config ] && . /home/jupyter/jupyter.config + +[ -z $IP ] && IP=$(ip address show dev eth0 | grep -P -o '10\.[0-9]*\.[0-9]*\.[0-9]*(?=/)') + +DAEMON_OPTS="--no-browser --user=$USER --port=$PORT --cookie-name=$COOKIE_NAME --base-url=$BASE_URL --hub-prefix=$HUB_PREFIX --hub-api-url=$HUB_API_URL --ip=$IP --debug" + +. /lib/lsb/init-functions + +########### + +start-stop-daemon --start --oknodo --background -d $RUN_DIR --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS diff --git a/tools/update-basefs.sh b/tools/update-basefs.sh new file mode 100755 index 0000000..a19d6e0 --- /dev/null +++ b/tools/update-basefs.sh @@ -0,0 +1,106 @@ +#!/bin/sh + +## WARNING +## This sript is just for my own convenience . my image is +## based on Ubuntu xenial. I did not test it for other distros. +## Therefore this script may not work for your basefs image. +## + + +if [ "$1" != "-y" ] ; then + echo "This script will update your basefs. backup it first." + echo "then run: $0 -y" + exit 1 +fi + + +# READ docklet.conf + +FS_PREFIX=/opt/docklet + +BASEFS=$FS_PREFIX/local/basefs + +CONF=../conf/docklet.conf + +echo "Reading $CONF" + +if [ -f $CONF ] ; then + . $CONF + BASEFS=$FS_PREFIX/local/basefs + echo "$CONF exit, basefs=$BASEFS" +else + echo "$CONF not exist, default basefs=$BASEFS" +fi + +if [ ! -d $BASEFS ] ; then + echo "Checking $BASEFS: not exist, FAIL" + exit 1 +else + echo "Checking $BASEFS: exist. " +fi + +echo "[*] Copying start_jupyter.sh to $BASEFS/home/jupyter" + +mkdir -p $BASEFS/home/jupyter + +cp start_jupyter.sh $BASEFS/home/jupyter + +echo "" + +echo "[*] Changing $BASEFS/etc/network/interfaces using static" + +echo "Original network/interfaces is" + +cat $BASEFS/etc/network/interfaces | sed 's/^/OLD /' + +sed -i -- 's/dhcp/static/g' $BASEFS/etc/network/interfaces + +# setting resolv.conf, use your own resolv.conf for your image +echo "[*] Setting $BASEFS/etc/resolv.conf" +cp resolv.conf $BASEFS/etc/resolvconf/resolv.conf.d/base + +echo "[*] Masking console-getty.service" +chroot $BASEFS systemctl mask console-getty.service + +echo "[*] Masking system-journald.service" +chroot $BASEFS systemctl mask systemd-journald.service + +echo "[*] Masking system-logind.service" +chroot $BASEFS systemctl mask systemd-logind.service + +echo "[*] Masking dbus.service" +chroot $BASEFS systemctl mask dbus.service + +echo "[*] Disabling apache2 service(if installed)" +chroot $BASEFS update-rc.d apache2 disable + +echo "[*] Disabling ondemand service(if installed)" +chroot $BASEFS update-rc.d ondemand disable + +echo "[*] Disabling dbus service(if installed)" +chroot $BASEFS update-rc.d dbus disable + +echo "[*] Disabling mysql service(if installed)" +chroot $BASEFS update-rc.d mysql disable + +echo "[*] Disabling nginx service(if installed)" +chroot $BASEFS update-rc.d nginx disable + +echo "[*] Setting worker_processes of nginx to 1(if installed)" +[ -f $BASEFS/etc/nginx/nginx.conf ] && sed -i -- 's/worker_processes\ auto/worker_processes\ 1/g' $BASEFS/etc/nginx/nginx.conf + +echo "[*] Deleting default /etc/nginx/sites-enabled/default" +rm -f $BASEFS/etc/nginx/sites-enabled/default + +echo "[*] Copying vimrc.local to $BASEFS/etc/vim/" +cp vimrc.local $BASEFS/etc/vim + +echo "[*] Copying pip.conf to $BASEFS/root/.pip/" +mkdir -p $BASEFS/root/.pip/ +cp pip.conf $BASEFS/root/.pip + +echo "[*] Copying npmrc to $BASEFS/root/.npmrc" +cp npmrc $BASEFS/root/.npmrc + +echo "[*] Copying DOCKLET_NOTES.txt to $BASEFS/root/DOCKLET_NOTES.txt" +cp DOCKLET_NOTES.txt $BASEFS/root/ diff --git a/tools/vimrc.local b/tools/vimrc.local new file mode 100644 index 0000000..d9d78c4 --- /dev/null +++ b/tools/vimrc.local @@ -0,0 +1,15 @@ +syntax on + +set smarttab expandtab sw=4 ts=4 + +set sm ai + +set hlsearch + +set wildchar= wildmenu wildmode=full + +set enc=utf-8 +set fileencoding=utf-8 +set fileencodings=utf-8,cp936,euc-cn,ascii + +filetype indent on diff --git a/web/static/avatar/default.png b/web/static/avatar/default.png new file mode 100644 index 0000000..caaef92 Binary files /dev/null and b/web/static/avatar/default.png differ diff --git a/web/static/css/docklet.css b/web/static/css/docklet.css new file mode 100644 index 0000000..fa54f98 --- /dev/null +++ b/web/static/css/docklet.css @@ -0,0 +1,54 @@ +.btn-outline, .btn-outline-default, .badge-outline, .badge-outline-default, .label-outline, .label-outline-default { + border: 1px solid #AAB2BD; + background-color: transparent; + color: #434A54; +} +.btn-outline-success, .badge-outline-success, .label-outline-success { + border: 1px solid #1C84C6; + background-color: transparent; + color: #1C84C6; +} +.btn-outline-warning, .badge-outline-warning, .label-outline-warning { + border: 1px solid #F8AC59; + background-color: transparent; + color: #F8AC59; +} + +.btn-outline-default:hover, +.btn-outline:hover { + border: 1px solid #AAB2BD; + background-color: #AAB2BD; + color: #434A54; +} + +.btn-outline-success:hover { + border: 1px solid #1C84C6; + background-color: #1C84C6; + color: #FFFFFF; +} + +.btn-outline-warning:hover { + border: 1px solid #F8AC59; + background-color: #F8AC59; + color: #FFFFFF; +} +.docklet-red-block{ + background-color: #EB4235; + color: #FFFFFF; +} + +.docklet-green-block{ + background-color: #7DB600; + color: #FFFFFF; +} + +.docklet-yellow-block{ + background-color: #FABC05; + color: #FFFFFF; +} + +.docklet-blue-block{ + background-color: #4185F6; + color: #FFFFFF; +} + diff --git a/web/static/dist/css/AdminLTE.css b/web/static/dist/css/AdminLTE.css new file mode 100644 index 0000000..6fd65f4 --- /dev/null +++ b/web/static/dist/css/AdminLTE.css @@ -0,0 +1,4914 @@ +/*! + * AdminLTE v2.3.2 + * Author: Almsaeed Studio + * Website: Almsaeed Studio + * License: Open source - MIT + * Please visit http://opensource.org/licenses/MIT for more information +!*/ +/* + * Core: General Layout Style + * ------------------------- + */ +html, +body { + min-height: 100%; +} +.layout-boxed html, +.layout-boxed body { + height: 100%; +} +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif; + font-weight: 400; + overflow-x: hidden; + overflow-y: auto; +} +/* Layout */ +.wrapper { + min-height: 100%; + position: relative; + overflow: hidden; +} +.wrapper:before, +.wrapper:after { + content: " "; + display: table; +} +.wrapper:after { + clear: both; +} +.layout-boxed .wrapper { + max-width: 1250px; + margin: 0 auto; + min-height: 100%; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.5); + position: relative; +} +.layout-boxed { + background: url('../img/boxed-bg.jpg') repeat fixed; +} +/* + * Content Wrapper - contains the main content + * ```.right-side has been deprecated as of v2.0.0 in favor of .content-wrapper ``` + */ +.content-wrapper, +.right-side, +.main-footer { + -webkit-transition: -webkit-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + -moz-transition: -moz-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + -o-transition: -o-transform 0.3s ease-in-out, margin 0.3s ease-in-out; + transition: transform 0.3s ease-in-out, margin 0.3s ease-in-out; + margin-left: 230px; + z-index: 820; +} +.layout-top-nav .content-wrapper, +.layout-top-nav .right-side, +.layout-top-nav .main-footer { + margin-left: 0; +} +@media (max-width: 767px) { + .content-wrapper, + .right-side, + .main-footer { + margin-left: 0; + } +} +@media (min-width: 768px) { + .sidebar-collapse .content-wrapper, + .sidebar-collapse .right-side, + .sidebar-collapse .main-footer { + margin-left: 0; + } +} +@media (max-width: 767px) { + .sidebar-open .content-wrapper, + .sidebar-open .right-side, + .sidebar-open .main-footer { + -webkit-transform: translate(230px, 0); + -ms-transform: translate(230px, 0); + -o-transform: translate(230px, 0); + transform: translate(230px, 0); + } +} +.content-wrapper, +.right-side { + min-height: 100%; + background-color: #ecf0f5; + z-index: 800; +} +.main-footer { + background: #fff; + padding: 15px; + color: #444; + border-top: 1px solid #d2d6de; +} +/* Fixed layout */ +.fixed .main-header, +.fixed .main-sidebar, +.fixed .left-side { + position: fixed; +} +.fixed .main-header { + top: 0; + right: 0; + left: 0; +} +.fixed .content-wrapper, +.fixed .right-side { + padding-top: 50px; +} +@media (max-width: 767px) { + .fixed .content-wrapper, + .fixed .right-side { + padding-top: 100px; + } +} +.fixed.layout-boxed .wrapper { + max-width: 100%; +} +body.hold-transition .content-wrapper, +body.hold-transition .right-side, +body.hold-transition .main-footer, +body.hold-transition .main-sidebar, +body.hold-transition .left-side, +body.hold-transition .main-header > .navbar, +body.hold-transition .main-header .logo { + /* Fix for IE */ + -webkit-transition: none; + -o-transition: none; + transition: none; +} +/* Content */ +.content { + min-height: 250px; + padding: 15px; + margin-right: auto; + margin-left: auto; + padding-left: 15px; + padding-right: 15px; +} +/* H1 - H6 font */ +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: 'Source Sans Pro', sans-serif; +} +/* General Links */ +a { + color: #3c8dbc; +} +a:hover, +a:active, +a:focus { + outline: none; + text-decoration: none; + color: #72afd2; +} +/* Page Header */ +.page-header { + margin: 10px 0 20px 0; + font-size: 22px; +} +.page-header > small { + color: #666; + display: block; + margin-top: 5px; +} +/* + * Component: Main Header + * ---------------------- + */ +.main-header { + position: relative; + max-height: 100px; + z-index: 1030; +} +.main-header > .navbar { + -webkit-transition: margin-left 0.3s ease-in-out; + -o-transition: margin-left 0.3s ease-in-out; + transition: margin-left 0.3s ease-in-out; + margin-bottom: 0; + margin-left: 230px; + border: none; + min-height: 50px; + border-radius: 0; +} +.layout-top-nav .main-header > .navbar { + margin-left: 0; +} +.main-header #navbar-search-input.form-control { + background: rgba(255, 255, 255, 0.2); + border-color: transparent; +} +.main-header #navbar-search-input.form-control:focus, +.main-header #navbar-search-input.form-control:active { + border-color: rgba(0, 0, 0, 0.1); + background: rgba(255, 255, 255, 0.9); +} +.main-header #navbar-search-input.form-control::-moz-placeholder { + color: #ccc; + opacity: 1; +} +.main-header #navbar-search-input.form-control:-ms-input-placeholder { + color: #ccc; +} +.main-header #navbar-search-input.form-control::-webkit-input-placeholder { + color: #ccc; +} +.main-header .navbar-custom-menu, +.main-header .navbar-right { + float: right; +} +@media (max-width: 991px) { + .main-header .navbar-custom-menu a, + .main-header .navbar-right a { + color: inherit; + background: transparent; + } +} +@media (max-width: 767px) { + .main-header .navbar-right { + float: none; + } + .navbar-collapse .main-header .navbar-right { + margin: 7.5px -15px; + } + .main-header .navbar-right > li { + color: inherit; + border: 0; + } +} +.main-header .sidebar-toggle { + float: left; + background-color: transparent; + background-image: none; + padding: 15px 15px; + font-family: fontAwesome; +} +.main-header .sidebar-toggle:before { + content: "\f0c9"; +} +.main-header .sidebar-toggle:hover { + color: #fff; +} +.main-header .sidebar-toggle:focus, +.main-header .sidebar-toggle:active { + background: transparent; +} +.main-header .sidebar-toggle .icon-bar { + display: none; +} +.main-header .navbar .nav > li.user > a > .fa, +.main-header .navbar .nav > li.user > a > .glyphicon, +.main-header .navbar .nav > li.user > a > .ion { + margin-right: 5px; +} +.main-header .navbar .nav > li > a > .label { + position: absolute; + top: 9px; + right: 7px; + text-align: center; + font-size: 9px; + padding: 2px 3px; + line-height: .9; +} +.main-header .logo { + -webkit-transition: width 0.3s ease-in-out; + -o-transition: width 0.3s ease-in-out; + transition: width 0.3s ease-in-out; + display: block; + float: left; + height: 50px; + font-size: 20px; + line-height: 50px; + text-align: center; + width: 230px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + padding: 0 15px; + font-weight: 300; + overflow: hidden; +} +.main-header .logo .logo-lg { + display: block; +} +.main-header .logo .logo-mini { + display: none; +} +.main-header .navbar-brand { + color: #fff; +} +.content-header { + position: relative; + padding: 15px 15px 0 15px; +} +.content-header > h1 { + margin: 0; + font-size: 24px; +} +.content-header > h1 > small { + font-size: 15px; + display: inline-block; + padding-left: 4px; + font-weight: 300; +} +.content-header > .breadcrumb { + float: right; + background: transparent; + margin-top: 0; + margin-bottom: 0; + font-size: 12px; + padding: 7px 5px; + position: absolute; + top: 15px; + right: 10px; + border-radius: 2px; +} +.content-header > .breadcrumb > li > a { + color: #444; + text-decoration: none; + display: inline-block; +} +.content-header > .breadcrumb > li > a > .fa, +.content-header > .breadcrumb > li > a > .glyphicon, +.content-header > .breadcrumb > li > a > .ion { + margin-right: 5px; +} +.content-header > .breadcrumb > li + li:before { + content: '>\00a0'; +} +@media (max-width: 991px) { + .content-header > .breadcrumb { + position: relative; + margin-top: 5px; + top: 0; + right: 0; + float: none; + background: #d2d6de; + padding-left: 10px; + } + .content-header > .breadcrumb li:before { + color: #97a0b3; + } +} +.navbar-toggle { + color: #fff; + border: 0; + margin: 0; + padding: 15px 15px; +} +@media (max-width: 991px) { + .navbar-custom-menu .navbar-nav > li { + float: left; + } + .navbar-custom-menu .navbar-nav { + margin: 0; + float: left; + } + .navbar-custom-menu .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + line-height: 20px; + } +} +@media (max-width: 767px) { + .main-header { + position: relative; + } + .main-header .logo, + .main-header .navbar { + width: 100%; + float: none; + } + .main-header .navbar { + margin: 0; + } + .main-header .navbar-custom-menu { + float: right; + } +} +@media (max-width: 991px) { + .navbar-collapse.pull-left { + float: none !important; + } + .navbar-collapse.pull-left + .navbar-custom-menu { + display: block; + position: absolute; + top: 0; + right: 40px; + } +} +/* + * Component: Sidebar + * ------------------ + */ +.main-sidebar, +.left-side { + position: absolute; + top: 0; + left: 0; + padding-top: 50px; + min-height: 100%; + width: 230px; + z-index: 810; + -webkit-transition: -webkit-transform 0.3s ease-in-out, width 0.3s ease-in-out; + -moz-transition: -moz-transform 0.3s ease-in-out, width 0.3s ease-in-out; + -o-transition: -o-transform 0.3s ease-in-out, width 0.3s ease-in-out; + transition: transform 0.3s ease-in-out, width 0.3s ease-in-out; +} +@media (max-width: 767px) { + .main-sidebar, + .left-side { + padding-top: 100px; + } +} +@media (max-width: 767px) { + .main-sidebar, + .left-side { + -webkit-transform: translate(-230px, 0); + -ms-transform: translate(-230px, 0); + -o-transform: translate(-230px, 0); + transform: translate(-230px, 0); + } +} +@media (min-width: 768px) { + .sidebar-collapse .main-sidebar, + .sidebar-collapse .left-side { + -webkit-transform: translate(-230px, 0); + -ms-transform: translate(-230px, 0); + -o-transform: translate(-230px, 0); + transform: translate(-230px, 0); + } +} +@media (max-width: 767px) { + .sidebar-open .main-sidebar, + .sidebar-open .left-side { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + } +} +.sidebar { + padding-bottom: 10px; +} +.sidebar-form input:focus { + border-color: transparent; +} +.user-panel { + position: relative; + width: 100%; + padding: 10px; + overflow: hidden; +} +.user-panel:before, +.user-panel:after { + content: " "; + display: table; +} +.user-panel:after { + clear: both; +} +.user-panel > .image > img { + width: 100%; + max-width: 45px; + height: auto; +} +.user-panel > .info { + padding: 5px 5px 5px 15px; + line-height: 1; + position: absolute; + left: 55px; +} +.user-panel > .info > p { + font-weight: 600; + margin-bottom: 9px; +} +.user-panel > .info > a { + text-decoration: none; + padding-right: 5px; + margin-top: 3px; + font-size: 11px; +} +.user-panel > .info > a > .fa, +.user-panel > .info > a > .ion, +.user-panel > .info > a > .glyphicon { + margin-right: 3px; +} +.sidebar-menu { + list-style: none; + margin: 0; + padding: 0; +} +.sidebar-menu > li { + position: relative; + margin: 0; + padding: 0; +} +.sidebar-menu > li > a { + padding: 12px 5px 12px 15px; + display: block; +} +.sidebar-menu > li > a > .fa, +.sidebar-menu > li > a > .glyphicon, +.sidebar-menu > li > a > .ion { + width: 20px; +} +.sidebar-menu > li .label, +.sidebar-menu > li .badge { + margin-top: 3px; + margin-right: 5px; +} +.sidebar-menu li.header { + padding: 10px 25px 10px 15px; + font-size: 12px; +} +.sidebar-menu li > a > .fa-angle-left { + width: auto; + height: auto; + padding: 0; + margin-right: 10px; + margin-top: 3px; +} +.sidebar-menu li.active > a > .fa-angle-left { + -webkit-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + transform: rotate(-90deg); +} +.sidebar-menu li.active > .treeview-menu { + display: block; +} +.sidebar-menu .treeview-menu { + display: none; + list-style: none; + padding: 0; + margin: 0; + padding-left: 5px; +} +.sidebar-menu .treeview-menu .treeview-menu { + padding-left: 20px; +} +.sidebar-menu .treeview-menu > li { + margin: 0; +} +.sidebar-menu .treeview-menu > li > a { + padding: 5px 5px 5px 15px; + display: block; + font-size: 14px; +} +.sidebar-menu .treeview-menu > li > a > .fa, +.sidebar-menu .treeview-menu > li > a > .glyphicon, +.sidebar-menu .treeview-menu > li > a > .ion { + width: 20px; +} +.sidebar-menu .treeview-menu > li > a > .fa-angle-left, +.sidebar-menu .treeview-menu > li > a > .fa-angle-down { + width: auto; +} +/* + * Component: Sidebar Mini + */ +@media (min-width: 768px) { + .sidebar-mini.sidebar-collapse .content-wrapper, + .sidebar-mini.sidebar-collapse .right-side, + .sidebar-mini.sidebar-collapse .main-footer { + margin-left: 50px !important; + z-index: 840; + } + .sidebar-mini.sidebar-collapse .main-sidebar { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + width: 50px !important; + z-index: 850; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li { + position: relative; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a { + margin-right: 0; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span { + border-top-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:not(.treeview) > a > span { + border-bottom-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + padding-top: 5px; + padding-bottom: 5px; + border-bottom-right-radius: 4px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span:not(.pull-right), + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu { + display: block !important; + position: absolute; + width: 180px; + left: 50px; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span { + top: 0; + margin-left: -3px; + padding: 12px 5px 12px 20px; + background-color: inherit; + } + .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu { + top: 44px; + margin-left: 0; + } + .sidebar-mini.sidebar-collapse .main-sidebar .user-panel > .info, + .sidebar-mini.sidebar-collapse .sidebar-form, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu, + .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > .pull-right, + .sidebar-mini.sidebar-collapse .sidebar-menu li.header { + display: none !important; + -webkit-transform: translateZ(0); + } + .sidebar-mini.sidebar-collapse .main-header .logo { + width: 50px; + } + .sidebar-mini.sidebar-collapse .main-header .logo > .logo-mini { + display: block; + margin-left: -15px; + margin-right: -15px; + font-size: 18px; + } + .sidebar-mini.sidebar-collapse .main-header .logo > .logo-lg { + display: none; + } + .sidebar-mini.sidebar-collapse .main-header .navbar { + margin-left: 50px; + } +} +.sidebar-menu, +.main-sidebar .user-panel, +.sidebar-menu > li.header { + white-space: nowrap; + overflow: hidden; +} +.sidebar-menu:hover { + overflow: visible; +} +.sidebar-form, +.sidebar-menu > li.header { + overflow: hidden; + text-overflow: clip; +} +.sidebar-menu li > a { + position: relative; +} +.sidebar-menu li > a > .pull-right { + position: absolute; + right: 10px; + top: 50%; + margin-top: -7px; +} +/* + * Component: Control sidebar. By default, this is the right sidebar. + */ +.control-sidebar-bg { + position: fixed; + z-index: 1000; + bottom: 0; +} +.control-sidebar-bg, +.control-sidebar { + top: 0; + right: -230px; + width: 230px; + -webkit-transition: right 0.3s ease-in-out; + -o-transition: right 0.3s ease-in-out; + transition: right 0.3s ease-in-out; +} +.control-sidebar { + position: absolute; + padding-top: 50px; + z-index: 1010; +} +@media (max-width: 768px) { + .control-sidebar { + padding-top: 100px; + } +} +.control-sidebar > .tab-content { + padding: 10px 15px; +} +.control-sidebar.control-sidebar-open, +.control-sidebar.control-sidebar-open + .control-sidebar-bg { + right: 0; +} +.control-sidebar-open .control-sidebar-bg, +.control-sidebar-open .control-sidebar { + right: 0; +} +@media (min-width: 768px) { + .control-sidebar-open .content-wrapper, + .control-sidebar-open .right-side, + .control-sidebar-open .main-footer { + margin-right: 230px; + } +} +.nav-tabs.control-sidebar-tabs > li:first-of-type > a, +.nav-tabs.control-sidebar-tabs > li:first-of-type > a:hover, +.nav-tabs.control-sidebar-tabs > li:first-of-type > a:focus { + border-left-width: 0; +} +.nav-tabs.control-sidebar-tabs > li > a { + border-radius: 0; +} +.nav-tabs.control-sidebar-tabs > li > a, +.nav-tabs.control-sidebar-tabs > li > a:hover { + border-top: none; + border-right: none; + border-left: 1px solid transparent; + border-bottom: 1px solid transparent; +} +.nav-tabs.control-sidebar-tabs > li > a .icon { + font-size: 16px; +} +.nav-tabs.control-sidebar-tabs > li.active > a, +.nav-tabs.control-sidebar-tabs > li.active > a:hover, +.nav-tabs.control-sidebar-tabs > li.active > a:focus, +.nav-tabs.control-sidebar-tabs > li.active > a:active { + border-top: none; + border-right: none; + border-bottom: none; +} +@media (max-width: 768px) { + .nav-tabs.control-sidebar-tabs { + display: table; + } + .nav-tabs.control-sidebar-tabs > li { + display: table-cell; + } +} +.control-sidebar-heading { + font-weight: 400; + font-size: 16px; + padding: 10px 0; + margin-bottom: 10px; +} +.control-sidebar-subheading { + display: block; + font-weight: 400; + font-size: 14px; +} +.control-sidebar-menu { + list-style: none; + padding: 0; + margin: 0 -15px; +} +.control-sidebar-menu > li > a { + display: block; + padding: 10px 15px; +} +.control-sidebar-menu > li > a:before, +.control-sidebar-menu > li > a:after { + content: " "; + display: table; +} +.control-sidebar-menu > li > a:after { + clear: both; +} +.control-sidebar-menu > li > a > .control-sidebar-subheading { + margin-top: 0; +} +.control-sidebar-menu .menu-icon { + float: left; + width: 35px; + height: 35px; + border-radius: 50%; + text-align: center; + line-height: 35px; +} +.control-sidebar-menu .menu-info { + margin-left: 45px; + margin-top: 3px; +} +.control-sidebar-menu .menu-info > .control-sidebar-subheading { + margin: 0; +} +.control-sidebar-menu .menu-info > p { + margin: 0; + font-size: 11px; +} +.control-sidebar-menu .progress { + margin: 0; +} +.control-sidebar-dark { + color: #b8c7ce; +} +.control-sidebar-dark, +.control-sidebar-dark + .control-sidebar-bg { + background: #222d32; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs { + border-bottom: #1c2529; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a { + background: #181f23; + color: #b8c7ce; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus { + border-left-color: #141a1d; + border-bottom-color: #141a1d; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:active { + background: #1c2529; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover { + color: #fff; +} +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:hover, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:focus, +.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:active { + background: #222d32; + color: #fff; +} +.control-sidebar-dark .control-sidebar-heading, +.control-sidebar-dark .control-sidebar-subheading { + color: #fff; +} +.control-sidebar-dark .control-sidebar-menu > li > a:hover { + background: #1e282c; +} +.control-sidebar-dark .control-sidebar-menu > li > a .menu-info > p { + color: #b8c7ce; +} +.control-sidebar-light { + color: #5e5e5e; +} +.control-sidebar-light, +.control-sidebar-light + .control-sidebar-bg { + background: #f9fafc; + border-left: 1px solid #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs { + border-bottom: #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a { + background: #e8ecf4; + color: #444444; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus { + border-left-color: #d2d6de; + border-bottom-color: #d2d6de; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:active { + background: #eff1f7; +} +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:hover, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:focus, +.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:active { + background: #f9fafc; + color: #111; +} +.control-sidebar-light .control-sidebar-heading, +.control-sidebar-light .control-sidebar-subheading { + color: #111; +} +.control-sidebar-light .control-sidebar-menu { + margin-left: -14px; +} +.control-sidebar-light .control-sidebar-menu > li > a:hover { + background: #f4f4f5; +} +.control-sidebar-light .control-sidebar-menu > li > a .menu-info > p { + color: #5e5e5e; +} +/* + * Component: Dropdown menus + * ------------------------- + */ +/*Dropdowns in general*/ +.dropdown-menu { + box-shadow: none; + border-color: #eee; +} +.dropdown-menu > li > a { + color: #777; +} +.dropdown-menu > li > a > .glyphicon, +.dropdown-menu > li > a > .fa, +.dropdown-menu > li > a > .ion { + margin-right: 10px; +} +.dropdown-menu > li > a:hover { + background-color: #e1e3e9; + color: #333; +} +.dropdown-menu > .divider { + background-color: #eee; +} +.navbar-nav > .notifications-menu > .dropdown-menu, +.navbar-nav > .messages-menu > .dropdown-menu, +.navbar-nav > .tasks-menu > .dropdown-menu { + width: 280px; + padding: 0 0 0 0; + margin: 0; + top: 100%; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li, +.navbar-nav > .messages-menu > .dropdown-menu > li, +.navbar-nav > .tasks-menu > .dropdown-menu > li { + position: relative; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.header, +.navbar-nav > .messages-menu > .dropdown-menu > li.header, +.navbar-nav > .tasks-menu > .dropdown-menu > li.header { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + background-color: #ffffff; + padding: 7px 10px; + border-bottom: 1px solid #f4f4f4; + color: #444444; + font-size: 14px; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a, +.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a, +.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; + font-size: 12px; + background-color: #fff; + padding: 7px 10px; + border-bottom: 1px solid #eeeeee; + color: #444 !important; + text-align: center; +} +@media (max-width: 991px) { + .navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a, + .navbar-nav > .messages-menu > .dropdown-menu > li.footer > a, + .navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a { + background: #fff !important; + color: #444 !important; + } +} +.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a:hover, +.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a:hover, +.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a:hover { + text-decoration: none; + font-weight: normal; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu { + max-height: 200px; + margin: 0; + padding: 0; + list-style: none; + overflow-x: hidden; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a { + display: block; + white-space: nowrap; + /* Prevent text from breaking */ + border-bottom: 1px solid #f4f4f4; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a:hover, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:hover, +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a:hover { + background: #f4f4f4; + text-decoration: none; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a { + color: #444444; + overflow: hidden; + text-overflow: ellipsis; + padding: 10px; +} +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .glyphicon, +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .fa, +.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .ion { + width: 20px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a { + margin: 0; + padding: 10px 10px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > div > img { + margin: auto 10px auto auto; + width: 40px; + height: 40px; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 { + padding: 0; + margin: 0 0 0 45px; + color: #444444; + font-size: 15px; + position: relative; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 > small { + color: #999999; + font-size: 10px; + position: absolute; + top: 0; + right: 0; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > p { + margin: 0 0 0 45px; + font-size: 12px; + color: #888888; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:before, +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after { + content: " "; + display: table; +} +.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after { + clear: both; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a { + padding: 10px; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > h3 { + font-size: 14px; + padding: 0; + margin: 0 0 10px 0; + color: #666666; +} +.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > .progress { + padding: 0; + margin: 0; +} +.navbar-nav > .user-menu > .dropdown-menu { + border-top-right-radius: 0; + border-top-left-radius: 0; + padding: 1px 0 0 0; + border-top-width: 0; + width: 280px; +} +.navbar-nav > .user-menu > .dropdown-menu, +.navbar-nav > .user-menu > .dropdown-menu > .user-body { + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header { + height: 175px; + padding: 10px; + text-align: center; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > img { + z-index: 5; + height: 90px; + width: 90px; + border: 3px solid; + border-color: transparent; + border-color: rgba(255, 255, 255, 0.2); +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p { + z-index: 5; + color: #fff; + color: rgba(255, 255, 255, 0.8); + font-size: 17px; + margin-top: 10px; +} +.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p > small { + display: block; + font-size: 12px; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body { + padding: 15px; + border-bottom: 1px solid #f4f4f4; + border-top: 1px solid #dddddd; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body:before, +.navbar-nav > .user-menu > .dropdown-menu > .user-body:after { + content: " "; + display: table; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body:after { + clear: both; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-body a { + color: #444 !important; +} +@media (max-width: 991px) { + .navbar-nav > .user-menu > .dropdown-menu > .user-body a { + background: #fff !important; + color: #444 !important; + } +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer { + background-color: #f9f9f9; + padding: 10px; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:before, +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after { + content: " "; + display: table; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after { + clear: both; +} +.navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default { + color: #666666; +} +@media (max-width: 991px) { + .navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default:hover { + background-color: #f9f9f9; + } +} +.navbar-nav > .user-menu .user-image { + float: left; + width: 25px; + height: 25px; + border-radius: 50%; + margin-right: 10px; + margin-top: -2px; +} +@media (max-width: 767px) { + .navbar-nav > .user-menu .user-image { + float: none; + margin-right: 0; + margin-top: -8px; + line-height: 10px; + } +} +/* Add fade animation to dropdown menus by appending + the class .animated-dropdown-menu to the .dropdown-menu ul (or ol)*/ +.open:not(.dropup) > .animated-dropdown-menu { + backface-visibility: visible !important; + -webkit-animation: flipInX 0.7s both; + -o-animation: flipInX 0.7s both; + animation: flipInX 0.7s both; +} +@keyframes flipInX { + 0% { + transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + transition-timing-function: ease-in; + opacity: 0; + } + 40% { + transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + transition-timing-function: ease-in; + } + 60% { + transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + transform: perspective(400px); + } +} +@-webkit-keyframes flipInX { + 0% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); + -webkit-transition-timing-function: ease-in; + opacity: 0; + } + 40% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); + -webkit-transition-timing-function: ease-in; + } + 60% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); + opacity: 1; + } + 80% { + -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); + } + 100% { + -webkit-transform: perspective(400px); + } +} +/* Fix dropdown menu in navbars */ +.navbar-custom-menu > .navbar-nav > li { + position: relative; +} +.navbar-custom-menu > .navbar-nav > li > .dropdown-menu { + position: absolute; + right: 0; + left: auto; +} +@media (max-width: 991px) { + .navbar-custom-menu > .navbar-nav { + float: right; + } + .navbar-custom-menu > .navbar-nav > li { + position: static; + } + .navbar-custom-menu > .navbar-nav > li > .dropdown-menu { + position: absolute; + right: 5%; + left: auto; + border: 1px solid #ddd; + background: #fff; + } +} +/* + * Component: Form + * --------------- + */ +.form-control { + border-radius: 0; + box-shadow: none; + border-color: #d2d6de; +} +.form-control:focus { + border-color: #3c8dbc; + box-shadow: none; +} +.form-control::-moz-placeholder, +.form-control:-ms-input-placeholder, +.form-control::-webkit-input-placeholder { + color: #bbb; + opacity: 1; +} +.form-control:not(select) { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} +.form-group.has-success label { + color: #00a65a; +} +.form-group.has-success .form-control { + border-color: #00a65a; + box-shadow: none; +} +.form-group.has-success .help-block { + color: #00a65a; +} +.form-group.has-warning label { + color: #f39c12; +} +.form-group.has-warning .form-control { + border-color: #f39c12; + box-shadow: none; +} +.form-group.has-warning .help-block { + color: #f39c12; +} +.form-group.has-error label { + color: #dd4b39; +} +.form-group.has-error .form-control { + border-color: #dd4b39; + box-shadow: none; +} +.form-group.has-error .help-block { + color: #dd4b39; +} +/* Input group */ +.input-group .input-group-addon { + border-radius: 0; + border-color: #d2d6de; + background-color: #fff; +} +/* button groups */ +.btn-group-vertical .btn.btn-flat:first-of-type, +.btn-group-vertical .btn.btn-flat:last-of-type { + border-radius: 0; +} +.icheck > label { + padding-left: 0; +} +/* support Font Awesome icons in form-control */ +.form-control-feedback.fa { + line-height: 34px; +} +.input-lg + .form-control-feedback.fa, +.input-group-lg + .form-control-feedback.fa, +.form-group-lg .form-control + .form-control-feedback.fa { + line-height: 46px; +} +.input-sm + .form-control-feedback.fa, +.input-group-sm + .form-control-feedback.fa, +.form-group-sm .form-control + .form-control-feedback.fa { + line-height: 30px; +} +/* + * Component: Progress Bar + * ----------------------- + */ +.progress, +.progress > .progress-bar { + -webkit-box-shadow: none; + box-shadow: none; +} +.progress, +.progress > .progress-bar, +.progress .progress-bar, +.progress > .progress-bar .progress-bar { + border-radius: 1px; +} +/* size variation */ +.progress.sm, +.progress-sm { + height: 10px; +} +.progress.sm, +.progress-sm, +.progress.sm .progress-bar, +.progress-sm .progress-bar { + border-radius: 1px; +} +.progress.xs, +.progress-xs { + height: 7px; +} +.progress.xs, +.progress-xs, +.progress.xs .progress-bar, +.progress-xs .progress-bar { + border-radius: 1px; +} +.progress.xxs, +.progress-xxs { + height: 3px; +} +.progress.xxs, +.progress-xxs, +.progress.xxs .progress-bar, +.progress-xxs .progress-bar { + border-radius: 1px; +} +/* Vertical bars */ +.progress.vertical { + position: relative; + width: 30px; + height: 200px; + display: inline-block; + margin-right: 10px; +} +.progress.vertical > .progress-bar { + width: 100%; + position: absolute; + bottom: 0; +} +.progress.vertical.sm, +.progress.vertical.progress-sm { + width: 20px; +} +.progress.vertical.xs, +.progress.vertical.progress-xs { + width: 10px; +} +.progress.vertical.xxs, +.progress.vertical.progress-xxs { + width: 3px; +} +.progress-group .progress-text { + font-weight: 600; +} +.progress-group .progress-number { + float: right; +} +/* Remove margins from progress bars when put in a table */ +.table tr > td .progress { + margin: 0; +} +.progress-bar-light-blue, +.progress-bar-primary { + background-color: #3c8dbc; +} +.progress-striped .progress-bar-light-blue, +.progress-striped .progress-bar-primary { + 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: -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-bar-green, +.progress-bar-success { + background-color: #00a65a; +} +.progress-striped .progress-bar-green, +.progress-striped .progress-bar-success { + 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: -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-bar-aqua, +.progress-bar-info { + background-color: #00c0ef; +} +.progress-striped .progress-bar-aqua, +.progress-striped .progress-bar-info { + 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: -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-bar-yellow, +.progress-bar-warning { + background-color: #f39c12; +} +.progress-striped .progress-bar-yellow, +.progress-striped .progress-bar-warning { + 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: -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-bar-red, +.progress-bar-danger { + background-color: #dd4b39; +} +.progress-striped .progress-bar-red, +.progress-striped .progress-bar-danger { + 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: -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); +} +/* + * Component: Small Box + * -------------------- + */ +.small-box { + border-radius: 2px; + position: relative; + display: block; + margin-bottom: 20px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} +.small-box > .inner { + padding: 10px; +} +.small-box > .small-box-footer { + position: relative; + text-align: center; + padding: 3px 0; + color: #fff; + color: rgba(255, 255, 255, 0.8); + display: block; + z-index: 10; + background: rgba(0, 0, 0, 0.1); + text-decoration: none; +} +.small-box > .small-box-footer:hover { + color: #fff; + background: rgba(0, 0, 0, 0.15); +} +.small-box h3 { + font-size: 38px; + font-weight: bold; + margin: 0 0 10px 0; + white-space: nowrap; + padding: 0; +} +.small-box p { + font-size: 15px; +} +.small-box p > small { + display: block; + color: #f9f9f9; + font-size: 13px; + margin-top: 5px; +} +.small-box h3, +.small-box p { + z-index: 5; +} +.small-box .icon { + -webkit-transition: all 0.3s linear; + -o-transition: all 0.3s linear; + transition: all 0.3s linear; + position: absolute; + top: -10px; + right: 10px; + z-index: 0; + font-size: 90px; + color: rgba(0, 0, 0, 0.15); +} +.small-box:hover { + text-decoration: none; + color: #f9f9f9; +} +.small-box:hover .icon { + font-size: 95px; +} +@media (max-width: 767px) { + .small-box { + text-align: center; + } + .small-box .icon { + display: none; + } + .small-box p { + font-size: 12px; + } +} +/* + * Component: Box + * -------------- + */ +.box { + position: relative; + border-radius: 3px; + background: #ffffff; + border-top: 3px solid #d2d6de; + margin-bottom: 20px; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); +} +.box.box-primary { + border-top-color: #3c8dbc; +} +.box.box-info { + border-top-color: #00c0ef; +} +.box.box-danger { + border-top-color: #dd4b39; +} +.box.box-warning { + border-top-color: #f39c12; +} +.box.box-success { + border-top-color: #00a65a; +} +.box.box-default { + border-top-color: #d2d6de; +} +.box.collapsed-box .box-body, +.box.collapsed-box .box-footer { + display: none; +} +.box .nav-stacked > li { + border-bottom: 1px solid #f4f4f4; + margin: 0; +} +.box .nav-stacked > li:last-of-type { + border-bottom: none; +} +.box.height-control .box-body { + max-height: 300px; + overflow: auto; +} +.box .border-right { + border-right: 1px solid #f4f4f4; +} +.box .border-left { + border-left: 1px solid #f4f4f4; +} +.box.box-solid { + border-top: 0; +} +.box.box-solid > .box-header .btn.btn-default { + background: transparent; +} +.box.box-solid > .box-header .btn:hover, +.box.box-solid > .box-header a:hover { + background: rgba(0, 0, 0, 0.1); +} +.box.box-solid.box-default { + border: 1px solid #d2d6de; +} +.box.box-solid.box-default > .box-header { + color: #444444; + background: #d2d6de; + background-color: #d2d6de; +} +.box.box-solid.box-default > .box-header a, +.box.box-solid.box-default > .box-header .btn { + color: #444444; +} +.box.box-solid.box-primary { + border: 1px solid #3c8dbc; +} +.box.box-solid.box-primary > .box-header { + color: #ffffff; + background: #3c8dbc; + background-color: #3c8dbc; +} +.box.box-solid.box-primary > .box-header a, +.box.box-solid.box-primary > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-info { + border: 1px solid #00c0ef; +} +.box.box-solid.box-info > .box-header { + color: #ffffff; + background: #00c0ef; + background-color: #00c0ef; +} +.box.box-solid.box-info > .box-header a, +.box.box-solid.box-info > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-danger { + border: 1px solid #dd4b39; +} +.box.box-solid.box-danger > .box-header { + color: #ffffff; + background: #dd4b39; + background-color: #dd4b39; +} +.box.box-solid.box-danger > .box-header a, +.box.box-solid.box-danger > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-warning { + border: 1px solid #f39c12; +} +.box.box-solid.box-warning > .box-header { + color: #ffffff; + background: #f39c12; + background-color: #f39c12; +} +.box.box-solid.box-warning > .box-header a, +.box.box-solid.box-warning > .box-header .btn { + color: #ffffff; +} +.box.box-solid.box-success { + border: 1px solid #00a65a; +} +.box.box-solid.box-success > .box-header { + color: #ffffff; + background: #00a65a; + background-color: #00a65a; +} +.box.box-solid.box-success > .box-header a, +.box.box-solid.box-success > .box-header .btn { + color: #ffffff; +} +.box.box-solid > .box-header > .box-tools .btn { + border: 0; + box-shadow: none; +} +.box.box-solid[class*='bg'] > .box-header { + color: #fff; +} +.box .box-group > .box { + margin-bottom: 5px; +} +.box .knob-label { + text-align: center; + color: #333; + font-weight: 100; + font-size: 12px; + margin-bottom: 0.3em; +} +.box > .overlay, +.overlay-wrapper > .overlay, +.box > .loading-img, +.overlay-wrapper > .loading-img { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.box .overlay, +.overlay-wrapper .overlay { + z-index: 50; + background: rgba(255, 255, 255, 0.7); + border-radius: 3px; +} +.box .overlay > .fa, +.overlay-wrapper .overlay > .fa { + position: absolute; + top: 50%; + left: 50%; + margin-left: -15px; + margin-top: -15px; + color: #000; + font-size: 30px; +} +.box .overlay.dark, +.overlay-wrapper .overlay.dark { + background: rgba(0, 0, 0, 0.5); +} +.box-header:before, +.box-body:before, +.box-footer:before, +.box-header:after, +.box-body:after, +.box-footer:after { + content: " "; + display: table; +} +.box-header:after, +.box-body:after, +.box-footer:after { + clear: both; +} +.box-header { + color: #444; + display: block; + padding: 10px; + position: relative; +} +.box-header.with-border { + border-bottom: 1px solid #f4f4f4; +} +.collapsed-box .box-header.with-border { + border-bottom: none; +} +.box-header > .fa, +.box-header > .glyphicon, +.box-header > .ion, +.box-header .box-title { + display: inline-block; + font-size: 18px; + margin: 0; + line-height: 1; +} +.box-header > .fa, +.box-header > .glyphicon, +.box-header > .ion { + margin-right: 5px; +} +.box-header > .box-tools { + position: absolute; + right: 10px; + top: 5px; +} +.box-header > .box-tools [data-toggle="tooltip"] { + position: relative; +} +.box-header > .box-tools.pull-right .dropdown-menu { + right: 0; + left: auto; +} +.btn-box-tool { + padding: 5px; + font-size: 12px; + background: transparent; + color: #97a0b3; +} +.open .btn-box-tool, +.btn-box-tool:hover { + color: #606c84; +} +.btn-box-tool.btn:active { + box-shadow: none; +} +.box-body { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + padding: 10px; +} +.no-header .box-body { + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.box-body > .table { + margin-bottom: 0; +} +.box-body .fc { + margin-top: 5px; +} +.box-body .full-width-chart { + margin: -19px; +} +.box-body.no-padding .full-width-chart { + margin: -9px; +} +.box-body .box-pane { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 3px; +} +.box-body .box-pane-right { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 0; +} +.box-footer { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; + border-top: 1px solid #f4f4f4; + padding: 10px; + background-color: #ffffff; +} +.chart-legend { + margin: 10px 0; +} +@media (max-width: 991px) { + .chart-legend > li { + float: left; + margin-right: 10px; + } +} +.box-comments { + background: #f7f7f7; +} +.box-comments .box-comment { + padding: 8px 0; + border-bottom: 1px solid #eee; +} +.box-comments .box-comment:before, +.box-comments .box-comment:after { + content: " "; + display: table; +} +.box-comments .box-comment:after { + clear: both; +} +.box-comments .box-comment:last-of-type { + border-bottom: 0; +} +.box-comments .box-comment:first-of-type { + padding-top: 0; +} +.box-comments .box-comment img { + float: left; +} +.box-comments .comment-text { + margin-left: 40px; + color: #555; +} +.box-comments .username { + color: #444; + display: block; + font-weight: 600; +} +.box-comments .text-muted { + font-weight: 400; + font-size: 12px; +} +/* Widget: TODO LIST */ +.todo-list { + margin: 0; + padding: 0; + list-style: none; + overflow: auto; +} +.todo-list > li { + border-radius: 2px; + padding: 10px; + background: #f4f4f4; + margin-bottom: 2px; + border-left: 2px solid #e6e7e8; + color: #444; +} +.todo-list > li:last-of-type { + margin-bottom: 0; +} +.todo-list > li > input[type='checkbox'] { + margin: 0 10px 0 5px; +} +.todo-list > li .text { + display: inline-block; + margin-left: 5px; + font-weight: 600; +} +.todo-list > li .label { + margin-left: 10px; + font-size: 9px; +} +.todo-list > li .tools { + display: none; + float: right; + color: #dd4b39; +} +.todo-list > li .tools > .fa, +.todo-list > li .tools > .glyphicon, +.todo-list > li .tools > .ion { + margin-right: 5px; + cursor: pointer; +} +.todo-list > li:hover .tools { + display: inline-block; +} +.todo-list > li.done { + color: #999; +} +.todo-list > li.done .text { + text-decoration: line-through; + font-weight: 500; +} +.todo-list > li.done .label { + background: #d2d6de !important; +} +.todo-list .danger { + border-left-color: #dd4b39; +} +.todo-list .warning { + border-left-color: #f39c12; +} +.todo-list .info { + border-left-color: #00c0ef; +} +.todo-list .success { + border-left-color: #00a65a; +} +.todo-list .primary { + border-left-color: #3c8dbc; +} +.todo-list .handle { + display: inline-block; + cursor: move; + margin: 0 5px; +} +/* Chat widget (DEPRECATED - this will be removed in the next major release. Use Direct Chat instead)*/ +.chat { + padding: 5px 20px 5px 10px; +} +.chat .item { + margin-bottom: 10px; +} +.chat .item:before, +.chat .item:after { + content: " "; + display: table; +} +.chat .item:after { + clear: both; +} +.chat .item > img { + width: 40px; + height: 40px; + border: 2px solid transparent; + border-radius: 50%; +} +.chat .item > .online { + border: 2px solid #00a65a; +} +.chat .item > .offline { + border: 2px solid #dd4b39; +} +.chat .item > .message { + margin-left: 55px; + margin-top: -40px; +} +.chat .item > .message > .name { + display: block; + font-weight: 600; +} +.chat .item > .attachment { + border-radius: 3px; + background: #f4f4f4; + margin-left: 65px; + margin-right: 15px; + padding: 10px; +} +.chat .item > .attachment > h4 { + margin: 0 0 5px 0; + font-weight: 600; + font-size: 14px; +} +.chat .item > .attachment > p, +.chat .item > .attachment > .filename { + font-weight: 600; + font-size: 13px; + font-style: italic; + margin: 0; +} +.chat .item > .attachment:before, +.chat .item > .attachment:after { + content: " "; + display: table; +} +.chat .item > .attachment:after { + clear: both; +} +.box-input { + max-width: 200px; +} +.modal .panel-body { + color: #444; +} +/* + * Component: Info Box + * ------------------- + */ +.info-box { + display: block; + min-height: 90px; + background: #fff; + width: 100%; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 2px; + margin-bottom: 15px; +} +.info-box small { + font-size: 14px; +} +.info-box .progress { + background: rgba(0, 0, 0, 0.2); + margin: 5px -10px 5px -10px; + height: 2px; +} +.info-box .progress, +.info-box .progress .progress-bar { + border-radius: 0; +} +.info-box .progress .progress-bar { + background: #fff; +} +.info-box-icon { + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; + display: block; + float: left; + height: 90px; + width: 90px; + text-align: center; + font-size: 45px; + line-height: 90px; + background: rgba(0, 0, 0, 0.2); +} +.info-box-icon > img { + max-width: 100%; +} +.info-box-content { + padding: 5px 10px; + margin-left: 90px; +} +.info-box-number { + display: block; + font-weight: bold; + font-size: 18px; +} +.progress-description, +.info-box-text { + display: block; + font-size: 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.info-box-text { + text-transform: uppercase; +} +.info-box-more { + display: block; +} +.progress-description { + margin: 0; +} +/* + * Component: Timeline + * ------------------- + */ +.timeline { + position: relative; + margin: 0 0 30px 0; + padding: 0; + list-style: none; +} +.timeline:before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 4px; + background: #ddd; + left: 31px; + margin: 0; + border-radius: 2px; +} +.timeline > li { + position: relative; + margin-right: 10px; + margin-bottom: 15px; +} +.timeline > li:before, +.timeline > li:after { + content: " "; + display: table; +} +.timeline > li:after { + clear: both; +} +.timeline > li > .timeline-item { + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + margin-top: 0; + background: #fff; + color: #444; + margin-left: 60px; + margin-right: 15px; + padding: 0; + position: relative; +} +.timeline > li > .timeline-item > .time { + color: #999; + float: right; + padding: 10px; + font-size: 12px; +} +.timeline > li > .timeline-item > .timeline-header { + margin: 0; + color: #555; + border-bottom: 1px solid #f4f4f4; + padding: 10px; + font-size: 16px; + line-height: 1.1; +} +.timeline > li > .timeline-item > .timeline-header > a { + font-weight: 600; +} +.timeline > li > .timeline-item > .timeline-body, +.timeline > li > .timeline-item > .timeline-footer { + padding: 10px; +} +.timeline > li > .fa, +.timeline > li > .glyphicon, +.timeline > li > .ion { + width: 30px; + height: 30px; + font-size: 15px; + line-height: 30px; + position: absolute; + color: #666; + background: #d2d6de; + border-radius: 50%; + text-align: center; + left: 18px; + top: 0; +} +.timeline > .time-label > span { + font-weight: 600; + padding: 5px; + display: inline-block; + background-color: #fff; + border-radius: 4px; +} +.timeline-inverse > li > .timeline-item { + background: #f0f0f0; + border: 1px solid #ddd; + -webkit-box-shadow: none; + box-shadow: none; +} +.timeline-inverse > li > .timeline-item > .timeline-header { + border-bottom-color: #ddd; +} +/* + * Component: Button + * ----------------- + */ +.btn { + border-radius: 3px; + -webkit-box-shadow: none; + box-shadow: none; + border: 1px solid transparent; +} +.btn.uppercase { + text-transform: uppercase; +} +.btn.btn-flat { + border-radius: 0; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + border-width: 1px; +} +.btn:active { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn:focus { + outline: none; +} +.btn.btn-file { + position: relative; + overflow: hidden; +} +.btn.btn-file > input[type='file'] { + position: absolute; + top: 0; + right: 0; + min-width: 100%; + min-height: 100%; + font-size: 100px; + text-align: right; + opacity: 0; + filter: alpha(opacity=0); + outline: none; + background: white; + cursor: inherit; + display: block; +} +.btn-default { + background-color: #f4f4f4; + color: #444; + border-color: #ddd; +} +.btn-default:hover, +.btn-default:active, +.btn-default.hover { + background-color: #e7e7e7; +} +.btn-primary { + background-color: #3c8dbc; + border-color: #367fa9; +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.hover { + background-color: #367fa9; +} +.btn-success { + background-color: #00a65a; + border-color: #008d4c; +} +.btn-success:hover, +.btn-success:active, +.btn-success.hover { + background-color: #008d4c; +} +.btn-info { + background-color: #00c0ef; + border-color: #00acd6; +} +.btn-info:hover, +.btn-info:active, +.btn-info.hover { + background-color: #00acd6; +} +.btn-danger { + background-color: #dd4b39; + border-color: #d73925; +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.hover { + background-color: #d73925; +} +.btn-warning { + background-color: #f39c12; + border-color: #e08e0b; +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.hover { + background-color: #e08e0b; +} +.btn-outline { + border: 1px solid #fff; + background: transparent; + color: #fff; +} +.btn-outline:hover, +.btn-outline:focus, +.btn-outline:active { + color: rgba(255, 255, 255, 0.7); + border-color: rgba(255, 255, 255, 0.7); +} +.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn[class*='bg-']:hover { + -webkit-box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2); + box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2); +} +.btn-app { + border-radius: 3px; + position: relative; + padding: 15px 5px; + margin: 0 0 10px 10px; + min-width: 80px; + height: 60px; + text-align: center; + color: #666; + border: 1px solid #ddd; + background-color: #f4f4f4; + font-size: 12px; +} +.btn-app > .fa, +.btn-app > .glyphicon, +.btn-app > .ion { + font-size: 20px; + display: block; +} +.btn-app:hover { + background: #f4f4f4; + color: #444; + border-color: #aaa; +} +.btn-app:active, +.btn-app:focus { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} +.btn-app > .badge { + position: absolute; + top: -3px; + right: -10px; + font-size: 10px; + font-weight: 400; +} +/* + * Component: Callout + * ------------------ + */ +.callout { + border-radius: 3px; + margin: 0 0 20px 0; + padding: 15px 30px 15px 15px; + border-left: 5px solid #eee; +} +.callout a { + color: #fff; + text-decoration: underline; +} +.callout a:hover { + color: #eee; +} +.callout h4 { + margin-top: 0; + font-weight: 600; +} +.callout p:last-child { + margin-bottom: 0; +} +.callout code, +.callout .highlight { + background-color: #fff; +} +.callout.callout-danger { + border-color: #c23321; +} +.callout.callout-warning { + border-color: #c87f0a; +} +.callout.callout-info { + border-color: #0097bc; +} +.callout.callout-success { + border-color: #00733e; +} +/* + * Component: alert + * ---------------- + */ +.alert { + border-radius: 3px; +} +.alert h4 { + font-weight: 600; +} +.alert .icon { + margin-right: 10px; +} +.alert .close { + color: #000; + opacity: 0.2; + filter: alpha(opacity=20); +} +.alert .close:hover { + opacity: 0.5; + filter: alpha(opacity=50); +} +.alert a { + color: #fff; + text-decoration: underline; +} +.alert-success { + border-color: #008d4c; +} +.alert-danger, +.alert-error { + border-color: #d73925; +} +.alert-warning { + border-color: #e08e0b; +} +.alert-info { + border-color: #00acd6; +} +/* + * Component: Nav + * -------------- + */ +.nav > li > a:hover, +.nav > li > a:active, +.nav > li > a:focus { + color: #444; + background: #f7f7f7; +} +/* NAV PILLS */ +.nav-pills > li > a { + border-radius: 0; + border-top: 3px solid transparent; + color: #444; +} +.nav-pills > li > a > .fa, +.nav-pills > li > a > .glyphicon, +.nav-pills > li > a > .ion { + margin-right: 5px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + border-top-color: #3c8dbc; +} +.nav-pills > li.active > a { + font-weight: 600; +} +/* NAV STACKED */ +.nav-stacked > li > a { + border-radius: 0; + border-top: 0; + border-left: 3px solid transparent; + color: #444; +} +.nav-stacked > li.active > a, +.nav-stacked > li.active > a:hover { + background: transparent; + color: #444; + border-top: 0; + border-left-color: #3c8dbc; +} +.nav-stacked > li.header { + border-bottom: 1px solid #ddd; + color: #777; + margin-bottom: 10px; + padding: 5px 10px; + text-transform: uppercase; +} +/* NAV TABS */ +.nav-tabs-custom { + margin-bottom: 20px; + background: #fff; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; +} +.nav-tabs-custom > .nav-tabs { + margin: 0; + border-bottom-color: #f4f4f4; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.nav-tabs-custom > .nav-tabs > li { + border-top: 3px solid transparent; + margin-bottom: -2px; + margin-right: 5px; +} +.nav-tabs-custom > .nav-tabs > li > a { + color: #444; + border-radius: 0; +} +.nav-tabs-custom > .nav-tabs > li > a.text-muted { + color: #999; +} +.nav-tabs-custom > .nav-tabs > li > a, +.nav-tabs-custom > .nav-tabs > li > a:hover { + background: transparent; + margin: 0; +} +.nav-tabs-custom > .nav-tabs > li > a:hover { + color: #999; +} +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:hover, +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:focus, +.nav-tabs-custom > .nav-tabs > li:not(.active) > a:active { + border-color: transparent; +} +.nav-tabs-custom > .nav-tabs > li.active { + border-top-color: #3c8dbc; +} +.nav-tabs-custom > .nav-tabs > li.active > a, +.nav-tabs-custom > .nav-tabs > li.active:hover > a { + background-color: #fff; + color: #444; +} +.nav-tabs-custom > .nav-tabs > li.active > a { + border-top-color: transparent; + border-left-color: #f4f4f4; + border-right-color: #f4f4f4; +} +.nav-tabs-custom > .nav-tabs > li:first-of-type { + margin-left: 0; +} +.nav-tabs-custom > .nav-tabs > li:first-of-type.active > a { + border-left-color: transparent; +} +.nav-tabs-custom > .nav-tabs.pull-right { + float: none !important; +} +.nav-tabs-custom > .nav-tabs.pull-right > li { + float: right; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type { + margin-right: 0; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type > a { + border-left-width: 1px; +} +.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type.active > a { + border-left-color: #f4f4f4; + border-right-color: transparent; +} +.nav-tabs-custom > .nav-tabs > li.header { + line-height: 35px; + padding: 0 10px; + font-size: 20px; + color: #444; +} +.nav-tabs-custom > .nav-tabs > li.header > .fa, +.nav-tabs-custom > .nav-tabs > li.header > .glyphicon, +.nav-tabs-custom > .nav-tabs > li.header > .ion { + margin-right: 5px; +} +.nav-tabs-custom > .tab-content { + background: #fff; + padding: 10px; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.nav-tabs-custom .dropdown.open > a:active, +.nav-tabs-custom .dropdown.open > a:focus { + background: transparent; + color: #999; +} +.nav-tabs-custom.tab-primary > .nav-tabs > li.active { + border-top-color: #3c8dbc; +} +.nav-tabs-custom.tab-info > .nav-tabs > li.active { + border-top-color: #00c0ef; +} +.nav-tabs-custom.tab-danger > .nav-tabs > li.active { + border-top-color: #dd4b39; +} +.nav-tabs-custom.tab-warning > .nav-tabs > li.active { + border-top-color: #f39c12; +} +.nav-tabs-custom.tab-success > .nav-tabs > li.active { + border-top-color: #00a65a; +} +.nav-tabs-custom.tab-default > .nav-tabs > li.active { + border-top-color: #d2d6de; +} +/* PAGINATION */ +.pagination > li > a { + background: #fafafa; + color: #666; +} +.pagination.pagination-flat > li > a { + border-radius: 0 !important; +} +/* + * Component: Products List + * ------------------------ + */ +.products-list { + list-style: none; + margin: 0; + padding: 0; +} +.products-list > .item { + border-radius: 3px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + padding: 10px 0; + background: #fff; +} +.products-list > .item:before, +.products-list > .item:after { + content: " "; + display: table; +} +.products-list > .item:after { + clear: both; +} +.products-list .product-img { + float: left; +} +.products-list .product-img img { + width: 50px; + height: 50px; +} +.products-list .product-info { + margin-left: 60px; +} +.products-list .product-title { + font-weight: 600; +} +.products-list .product-description { + display: block; + color: #999; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.product-list-in-box > .item { + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; + border-bottom: 1px solid #f4f4f4; +} +.product-list-in-box > .item:last-of-type { + border-bottom-width: 0; +} +/* + * Component: Table + * ---------------- + */ +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + border-top: 1px solid #f4f4f4; +} +.table > thead > tr > th { + border-bottom: 2px solid #f4f4f4; +} +.table tr td .progress { + margin-top: 5px; +} +.table-bordered { + border: 1px solid #f4f4f4; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #f4f4f4; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table.no-border, +.table.no-border td, +.table.no-border th { + border: 0; +} +/* .text-center in tables */ +table.text-center, +table.text-center td, +table.text-center th { + text-align: center; +} +.table.align th { + text-align: left; +} +.table.align td { + text-align: right; +} +/* + * Component: Label + * ---------------- + */ +.label-default { + background-color: #d2d6de; + color: #444; +} +/* + * Component: Direct Chat + * ---------------------- + */ +.direct-chat .box-body { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + position: relative; + overflow-x: hidden; + padding: 0; +} +.direct-chat.chat-pane-open .direct-chat-contacts { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.direct-chat-messages { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); + padding: 10px; + height: 250px; + overflow: auto; +} +.direct-chat-msg, +.direct-chat-text { + display: block; +} +.direct-chat-msg { + margin-bottom: 10px; +} +.direct-chat-msg:before, +.direct-chat-msg:after { + content: " "; + display: table; +} +.direct-chat-msg:after { + clear: both; +} +.direct-chat-messages, +.direct-chat-contacts { + -webkit-transition: -webkit-transform 0.5s ease-in-out; + -moz-transition: -moz-transform 0.5s ease-in-out; + -o-transition: -o-transform 0.5s ease-in-out; + transition: transform 0.5s ease-in-out; +} +.direct-chat-text { + border-radius: 5px; + position: relative; + padding: 5px 10px; + background: #d2d6de; + border: 1px solid #d2d6de; + margin: 5px 0 0 50px; + color: #444444; +} +.direct-chat-text:after, +.direct-chat-text:before { + position: absolute; + right: 100%; + top: 15px; + border: solid transparent; + border-right-color: #d2d6de; + content: ' '; + height: 0; + width: 0; + pointer-events: none; +} +.direct-chat-text:after { + border-width: 5px; + margin-top: -5px; +} +.direct-chat-text:before { + border-width: 6px; + margin-top: -6px; +} +.right .direct-chat-text { + margin-right: 50px; + margin-left: 0; +} +.right .direct-chat-text:after, +.right .direct-chat-text:before { + right: auto; + left: 100%; + border-right-color: transparent; + border-left-color: #d2d6de; +} +.direct-chat-img { + border-radius: 50%; + float: left; + width: 40px; + height: 40px; +} +.right .direct-chat-img { + float: right; +} +.direct-chat-info { + display: block; + margin-bottom: 2px; + font-size: 12px; +} +.direct-chat-name { + font-weight: 600; +} +.direct-chat-timestamp { + color: #999; +} +.direct-chat-contacts-open .direct-chat-contacts { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.direct-chat-contacts { + -webkit-transform: translate(101%, 0); + -ms-transform: translate(101%, 0); + -o-transform: translate(101%, 0); + transform: translate(101%, 0); + position: absolute; + top: 0; + bottom: 0; + height: 250px; + width: 100%; + background: #222d32; + color: #fff; + overflow: auto; +} +.contacts-list > li { + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + padding: 10px; + margin: 0; +} +.contacts-list > li:before, +.contacts-list > li:after { + content: " "; + display: table; +} +.contacts-list > li:after { + clear: both; +} +.contacts-list > li:last-of-type { + border-bottom: none; +} +.contacts-list-img { + border-radius: 50%; + width: 40px; + float: left; +} +.contacts-list-info { + margin-left: 45px; + color: #fff; +} +.contacts-list-name, +.contacts-list-status { + display: block; +} +.contacts-list-name { + font-weight: 600; +} +.contacts-list-status { + font-size: 12px; +} +.contacts-list-date { + color: #aaa; + font-weight: normal; +} +.contacts-list-msg { + color: #999; +} +.direct-chat-danger .right > .direct-chat-text { + background: #dd4b39; + border-color: #dd4b39; + color: #ffffff; +} +.direct-chat-danger .right > .direct-chat-text:after, +.direct-chat-danger .right > .direct-chat-text:before { + border-left-color: #dd4b39; +} +.direct-chat-primary .right > .direct-chat-text { + background: #3c8dbc; + border-color: #3c8dbc; + color: #ffffff; +} +.direct-chat-primary .right > .direct-chat-text:after, +.direct-chat-primary .right > .direct-chat-text:before { + border-left-color: #3c8dbc; +} +.direct-chat-warning .right > .direct-chat-text { + background: #f39c12; + border-color: #f39c12; + color: #ffffff; +} +.direct-chat-warning .right > .direct-chat-text:after, +.direct-chat-warning .right > .direct-chat-text:before { + border-left-color: #f39c12; +} +.direct-chat-info .right > .direct-chat-text { + background: #00c0ef; + border-color: #00c0ef; + color: #ffffff; +} +.direct-chat-info .right > .direct-chat-text:after, +.direct-chat-info .right > .direct-chat-text:before { + border-left-color: #00c0ef; +} +.direct-chat-success .right > .direct-chat-text { + background: #00a65a; + border-color: #00a65a; + color: #ffffff; +} +.direct-chat-success .right > .direct-chat-text:after, +.direct-chat-success .right > .direct-chat-text:before { + border-left-color: #00a65a; +} +/* + * Component: Users List + * --------------------- + */ +.users-list > li { + width: 25%; + float: left; + padding: 10px; + text-align: center; +} +.users-list > li img { + border-radius: 50%; + max-width: 100%; + height: auto; +} +.users-list > li > a:hover, +.users-list > li > a:hover .users-list-name { + color: #999; +} +.users-list-name, +.users-list-date { + display: block; +} +.users-list-name { + font-weight: 600; + color: #444; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} +.users-list-date { + color: #999; + font-size: 12px; +} +/* + * Component: Carousel + * ------------------- + */ +.carousel-control.left, +.carousel-control.right { + background-image: none; +} +.carousel-control > .fa { + font-size: 40px; + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; + margin-top: -20px; +} +/* + * Component: modal + * ---------------- + */ +.modal { + background: rgba(0, 0, 0, 0.3); +} +.modal-content { + border-radius: 0; + -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + border: 0; +} +@media (min-width: 768px) { + .modal-content { + -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125); + } +} +.modal-header { + border-bottom-color: #f4f4f4; +} +.modal-footer { + border-top-color: #f4f4f4; +} +.modal-primary .modal-header, +.modal-primary .modal-footer { + border-color: #307095; +} +.modal-warning .modal-header, +.modal-warning .modal-footer { + border-color: #c87f0a; +} +.modal-info .modal-header, +.modal-info .modal-footer { + border-color: #0097bc; +} +.modal-success .modal-header, +.modal-success .modal-footer { + border-color: #00733e; +} +.modal-danger .modal-header, +.modal-danger .modal-footer { + border-color: #c23321; +} +/* + * Component: Social Widgets + * ------------------------- + */ +.box-widget { + border: none; + position: relative; +} +.widget-user .widget-user-header { + padding: 20px; + height: 120px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.widget-user .widget-user-username { + margin-top: 0; + margin-bottom: 5px; + font-size: 25px; + font-weight: 300; + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); +} +.widget-user .widget-user-desc { + margin-top: 0; +} +.widget-user .widget-user-image { + position: absolute; + top: 65px; + left: 50%; + margin-left: -45px; +} +.widget-user .widget-user-image > img { + width: 90px; + height: auto; + border: 3px solid #fff; +} +.widget-user .box-footer { + padding-top: 30px; +} +.widget-user-2 .widget-user-header { + padding: 20px; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} +.widget-user-2 .widget-user-username { + margin-top: 5px; + margin-bottom: 5px; + font-size: 25px; + font-weight: 300; +} +.widget-user-2 .widget-user-desc { + margin-top: 0; +} +.widget-user-2 .widget-user-username, +.widget-user-2 .widget-user-desc { + margin-left: 75px; +} +.widget-user-2 .widget-user-image > img { + width: 65px; + height: auto; + float: left; +} +/* + * Page: Mailbox + * ------------- + */ +.mailbox-messages > .table { + margin: 0; +} +.mailbox-controls { + padding: 5px; +} +.mailbox-controls.with-border { + border-bottom: 1px solid #f4f4f4; +} +.mailbox-read-info { + border-bottom: 1px solid #f4f4f4; + padding: 10px; +} +.mailbox-read-info h3 { + font-size: 20px; + margin: 0; +} +.mailbox-read-info h5 { + margin: 0; + padding: 5px 0 0 0; +} +.mailbox-read-time { + color: #999; + font-size: 13px; +} +.mailbox-read-message { + padding: 10px; +} +.mailbox-attachments li { + float: left; + width: 200px; + border: 1px solid #eee; + margin-bottom: 10px; + margin-right: 10px; +} +.mailbox-attachment-name { + font-weight: bold; + color: #666; +} +.mailbox-attachment-icon, +.mailbox-attachment-info, +.mailbox-attachment-size { + display: block; +} +.mailbox-attachment-info { + padding: 10px; + background: #f4f4f4; +} +.mailbox-attachment-size { + color: #999; + font-size: 12px; +} +.mailbox-attachment-icon { + text-align: center; + font-size: 65px; + color: #666; + padding: 20px 10px; +} +.mailbox-attachment-icon.has-img { + padding: 0; +} +.mailbox-attachment-icon.has-img > img { + max-width: 100%; + height: auto; +} +/* + * Page: Lock Screen + * ----------------- + */ +/* ADD THIS CLASS TO THE TAG */ +.lockscreen { + background: #d2d6de; +} +.lockscreen-logo { + font-size: 35px; + text-align: center; + margin-bottom: 25px; + font-weight: 300; +} +.lockscreen-logo a { + color: #444; +} +.lockscreen-wrapper { + max-width: 400px; + margin: 0 auto; + margin-top: 10%; +} +/* User name [optional] */ +.lockscreen .lockscreen-name { + text-align: center; + font-weight: 600; +} +/* Will contain the image and the sign in form */ +.lockscreen-item { + border-radius: 4px; + padding: 0; + background: #fff; + position: relative; + margin: 10px auto 30px auto; + width: 290px; +} +/* User image */ +.lockscreen-image { + border-radius: 50%; + position: absolute; + left: -10px; + top: -25px; + background: #fff; + padding: 5px; + z-index: 10; +} +.lockscreen-image > img { + border-radius: 50%; + width: 70px; + height: 70px; +} +/* Contains the password input and the login button */ +.lockscreen-credentials { + margin-left: 70px; +} +.lockscreen-credentials .form-control { + border: 0; +} +.lockscreen-credentials .btn { + background-color: #fff; + border: 0; + padding: 0 10px; +} +.lockscreen-footer { + margin-top: 10px; +} +/* + * Page: Login & Register + * ---------------------- + */ +.login-logo, +.register-logo { + font-size: 35px; + text-align: center; + margin-bottom: 25px; + font-weight: 300; +} +.login-logo a, +.register-logo a { + color: #444; +} +.login-page, +.register-page { + background: #d2d6de; +} +.login-box, +.register-box { + width: 360px; + margin: 7% auto; +} +@media (max-width: 768px) { + .login-box, + .register-box { + width: 90%; + margin-top: 20px; + } +} +.login-box-body, +.register-box-body { + background: #fff; + padding: 20px; + border-top: 0; + color: #666; +} +.login-box-body .form-control-feedback, +.register-box-body .form-control-feedback { + color: #777; +} +.login-box-msg, +.register-box-msg { + margin: 0; + text-align: center; + padding: 0 20px 20px 20px; +} +.social-auth-links { + margin: 10px 0; +} +/* + * Page: 400 and 500 error pages + * ------------------------------ + */ +.error-page { + width: 600px; + margin: 20px auto 0 auto; +} +@media (max-width: 991px) { + .error-page { + width: 100%; + } +} +.error-page > .headline { + float: left; + font-size: 100px; + font-weight: 300; +} +@media (max-width: 991px) { + .error-page > .headline { + float: none; + text-align: center; + } +} +.error-page > .error-content { + margin-left: 190px; + display: block; +} +@media (max-width: 991px) { + .error-page > .error-content { + margin-left: 0; + } +} +.error-page > .error-content > h3 { + font-weight: 300; + font-size: 25px; +} +@media (max-width: 991px) { + .error-page > .error-content > h3 { + text-align: center; + } +} +/* + * Page: Invoice + * ------------- + */ +.invoice { + position: relative; + background: #fff; + border: 1px solid #f4f4f4; + padding: 20px; + margin: 10px 25px; +} +.invoice-title { + margin-top: 0; +} +/* + * Page: Profile + * ------------- + */ +.profile-user-img { + margin: 0 auto; + width: 100px; + padding: 3px; + border: 3px solid #d2d6de; +} +.profile-username { + font-size: 21px; + margin-top: 5px; +} +.post { + border-bottom: 1px solid #d2d6de; + margin-bottom: 15px; + padding-bottom: 15px; + color: #666; +} +.post:last-of-type { + border-bottom: 0; + margin-bottom: 0; + padding-bottom: 0; +} +.post .user-block { + margin-bottom: 15px; +} +/* + * Social Buttons for Bootstrap + * + * Copyright 2013-2015 Panayiotis Lipiridis + * Licensed under the MIT License + * + * https://github.com/lipis/bootstrap-social + */ +.btn-social { + position: relative; + padding-left: 44px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.btn-social > :first-child { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 32px; + line-height: 34px; + font-size: 1.6em; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.2); +} +.btn-social.btn-lg { + padding-left: 61px; +} +.btn-social.btn-lg > :first-child { + line-height: 45px; + width: 45px; + font-size: 1.8em; +} +.btn-social.btn-sm { + padding-left: 38px; +} +.btn-social.btn-sm > :first-child { + line-height: 28px; + width: 28px; + font-size: 1.4em; +} +.btn-social.btn-xs { + padding-left: 30px; +} +.btn-social.btn-xs > :first-child { + line-height: 20px; + width: 20px; + font-size: 1.2em; +} +.btn-social-icon { + position: relative; + padding-left: 44px; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 34px; + width: 34px; + padding: 0; +} +.btn-social-icon > :first-child { + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 32px; + line-height: 34px; + font-size: 1.6em; + text-align: center; + border-right: 1px solid rgba(0, 0, 0, 0.2); +} +.btn-social-icon.btn-lg { + padding-left: 61px; +} +.btn-social-icon.btn-lg > :first-child { + line-height: 45px; + width: 45px; + font-size: 1.8em; +} +.btn-social-icon.btn-sm { + padding-left: 38px; +} +.btn-social-icon.btn-sm > :first-child { + line-height: 28px; + width: 28px; + font-size: 1.4em; +} +.btn-social-icon.btn-xs { + padding-left: 30px; +} +.btn-social-icon.btn-xs > :first-child { + line-height: 20px; + width: 20px; + font-size: 1.2em; +} +.btn-social-icon > :first-child { + border: none; + text-align: center; + width: 100%; +} +.btn-social-icon.btn-lg { + height: 45px; + width: 45px; + padding-left: 0; + padding-right: 0; +} +.btn-social-icon.btn-sm { + height: 30px; + width: 30px; + padding-left: 0; + padding-right: 0; +} +.btn-social-icon.btn-xs { + height: 22px; + width: 22px; + padding-left: 0; + padding-right: 0; +} +.btn-adn { + color: #ffffff; + background-color: #d87a68; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:focus, +.btn-adn.focus { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:hover { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:active, +.btn-adn.active, +.open > .dropdown-toggle.btn-adn { + color: #ffffff; + background-color: #ce563f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-adn:active, +.btn-adn.active, +.open > .dropdown-toggle.btn-adn { + background-image: none; +} +.btn-adn .badge { + color: #d87a68; + background-color: #ffffff; +} +.btn-bitbucket { + color: #ffffff; + background-color: #205081; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:focus, +.btn-bitbucket.focus { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:hover { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:active, +.btn-bitbucket.active, +.open > .dropdown-toggle.btn-bitbucket { + color: #ffffff; + background-color: #163758; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-bitbucket:active, +.btn-bitbucket.active, +.open > .dropdown-toggle.btn-bitbucket { + background-image: none; +} +.btn-bitbucket .badge { + color: #205081; + background-color: #ffffff; +} +.btn-dropbox { + color: #ffffff; + background-color: #1087dd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:focus, +.btn-dropbox.focus { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:hover { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:active, +.btn-dropbox.active, +.open > .dropdown-toggle.btn-dropbox { + color: #ffffff; + background-color: #0d6aad; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-dropbox:active, +.btn-dropbox.active, +.open > .dropdown-toggle.btn-dropbox { + background-image: none; +} +.btn-dropbox .badge { + color: #1087dd; + background-color: #ffffff; +} +.btn-facebook { + color: #ffffff; + background-color: #3b5998; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:focus, +.btn-facebook.focus { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:hover { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:active, +.btn-facebook.active, +.open > .dropdown-toggle.btn-facebook { + color: #ffffff; + background-color: #2d4373; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-facebook:active, +.btn-facebook.active, +.open > .dropdown-toggle.btn-facebook { + background-image: none; +} +.btn-facebook .badge { + color: #3b5998; + background-color: #ffffff; +} +.btn-flickr { + color: #ffffff; + background-color: #ff0084; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:focus, +.btn-flickr.focus { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:hover { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:active, +.btn-flickr.active, +.open > .dropdown-toggle.btn-flickr { + color: #ffffff; + background-color: #cc006a; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-flickr:active, +.btn-flickr.active, +.open > .dropdown-toggle.btn-flickr { + background-image: none; +} +.btn-flickr .badge { + color: #ff0084; + background-color: #ffffff; +} +.btn-foursquare { + color: #ffffff; + background-color: #f94877; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:focus, +.btn-foursquare.focus { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:hover { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:active, +.btn-foursquare.active, +.open > .dropdown-toggle.btn-foursquare { + color: #ffffff; + background-color: #f71752; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-foursquare:active, +.btn-foursquare.active, +.open > .dropdown-toggle.btn-foursquare { + background-image: none; +} +.btn-foursquare .badge { + color: #f94877; + background-color: #ffffff; +} +.btn-github { + color: #ffffff; + background-color: #444444; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:focus, +.btn-github.focus { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:hover { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:active, +.btn-github.active, +.open > .dropdown-toggle.btn-github { + color: #ffffff; + background-color: #2b2b2b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-github:active, +.btn-github.active, +.open > .dropdown-toggle.btn-github { + background-image: none; +} +.btn-github .badge { + color: #444444; + background-color: #ffffff; +} +.btn-google { + color: #ffffff; + background-color: #dd4b39; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:focus, +.btn-google.focus { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:hover { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:active, +.btn-google.active, +.open > .dropdown-toggle.btn-google { + color: #ffffff; + background-color: #c23321; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-google:active, +.btn-google.active, +.open > .dropdown-toggle.btn-google { + background-image: none; +} +.btn-google .badge { + color: #dd4b39; + background-color: #ffffff; +} +.btn-instagram { + color: #ffffff; + background-color: #3f729b; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:focus, +.btn-instagram.focus { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:hover { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:active, +.btn-instagram.active, +.open > .dropdown-toggle.btn-instagram { + color: #ffffff; + background-color: #305777; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-instagram:active, +.btn-instagram.active, +.open > .dropdown-toggle.btn-instagram { + background-image: none; +} +.btn-instagram .badge { + color: #3f729b; + background-color: #ffffff; +} +.btn-linkedin { + color: #ffffff; + background-color: #007bb6; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:focus, +.btn-linkedin.focus { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:hover { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:active, +.btn-linkedin.active, +.open > .dropdown-toggle.btn-linkedin { + color: #ffffff; + background-color: #005983; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-linkedin:active, +.btn-linkedin.active, +.open > .dropdown-toggle.btn-linkedin { + background-image: none; +} +.btn-linkedin .badge { + color: #007bb6; + background-color: #ffffff; +} +.btn-microsoft { + color: #ffffff; + background-color: #2672ec; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:focus, +.btn-microsoft.focus { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:hover { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:active, +.btn-microsoft.active, +.open > .dropdown-toggle.btn-microsoft { + color: #ffffff; + background-color: #125acd; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-microsoft:active, +.btn-microsoft.active, +.open > .dropdown-toggle.btn-microsoft { + background-image: none; +} +.btn-microsoft .badge { + color: #2672ec; + background-color: #ffffff; +} +.btn-openid { + color: #ffffff; + background-color: #f7931e; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:focus, +.btn-openid.focus { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:hover { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:active, +.btn-openid.active, +.open > .dropdown-toggle.btn-openid { + color: #ffffff; + background-color: #da7908; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-openid:active, +.btn-openid.active, +.open > .dropdown-toggle.btn-openid { + background-image: none; +} +.btn-openid .badge { + color: #f7931e; + background-color: #ffffff; +} +.btn-pinterest { + color: #ffffff; + background-color: #cb2027; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:focus, +.btn-pinterest.focus { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:hover { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:active, +.btn-pinterest.active, +.open > .dropdown-toggle.btn-pinterest { + color: #ffffff; + background-color: #9f191f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-pinterest:active, +.btn-pinterest.active, +.open > .dropdown-toggle.btn-pinterest { + background-image: none; +} +.btn-pinterest .badge { + color: #cb2027; + background-color: #ffffff; +} +.btn-reddit { + color: #000000; + background-color: #eff7ff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:focus, +.btn-reddit.focus { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:hover { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:active, +.btn-reddit.active, +.open > .dropdown-toggle.btn-reddit { + color: #000000; + background-color: #bcddff; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-reddit:active, +.btn-reddit.active, +.open > .dropdown-toggle.btn-reddit { + background-image: none; +} +.btn-reddit .badge { + color: #eff7ff; + background-color: #000000; +} +.btn-soundcloud { + color: #ffffff; + background-color: #ff5500; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:focus, +.btn-soundcloud.focus { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:hover { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:active, +.btn-soundcloud.active, +.open > .dropdown-toggle.btn-soundcloud { + color: #ffffff; + background-color: #cc4400; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-soundcloud:active, +.btn-soundcloud.active, +.open > .dropdown-toggle.btn-soundcloud { + background-image: none; +} +.btn-soundcloud .badge { + color: #ff5500; + background-color: #ffffff; +} +.btn-tumblr { + color: #ffffff; + background-color: #2c4762; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:focus, +.btn-tumblr.focus { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:hover { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:active, +.btn-tumblr.active, +.open > .dropdown-toggle.btn-tumblr { + color: #ffffff; + background-color: #1c2d3f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-tumblr:active, +.btn-tumblr.active, +.open > .dropdown-toggle.btn-tumblr { + background-image: none; +} +.btn-tumblr .badge { + color: #2c4762; + background-color: #ffffff; +} +.btn-twitter { + color: #ffffff; + background-color: #55acee; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:focus, +.btn-twitter.focus { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:hover { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:active, +.btn-twitter.active, +.open > .dropdown-toggle.btn-twitter { + color: #ffffff; + background-color: #2795e9; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-twitter:active, +.btn-twitter.active, +.open > .dropdown-toggle.btn-twitter { + background-image: none; +} +.btn-twitter .badge { + color: #55acee; + background-color: #ffffff; +} +.btn-vimeo { + color: #ffffff; + background-color: #1ab7ea; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:focus, +.btn-vimeo.focus { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:hover { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:active, +.btn-vimeo.active, +.open > .dropdown-toggle.btn-vimeo { + color: #ffffff; + background-color: #1295bf; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vimeo:active, +.btn-vimeo.active, +.open > .dropdown-toggle.btn-vimeo { + background-image: none; +} +.btn-vimeo .badge { + color: #1ab7ea; + background-color: #ffffff; +} +.btn-vk { + color: #ffffff; + background-color: #587ea3; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:focus, +.btn-vk.focus { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:hover { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:active, +.btn-vk.active, +.open > .dropdown-toggle.btn-vk { + color: #ffffff; + background-color: #466482; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-vk:active, +.btn-vk.active, +.open > .dropdown-toggle.btn-vk { + background-image: none; +} +.btn-vk .badge { + color: #587ea3; + background-color: #ffffff; +} +.btn-yahoo { + color: #ffffff; + background-color: #720e9e; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:focus, +.btn-yahoo.focus { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:hover { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:active, +.btn-yahoo.active, +.open > .dropdown-toggle.btn-yahoo { + color: #ffffff; + background-color: #500a6f; + border-color: rgba(0, 0, 0, 0.2); +} +.btn-yahoo:active, +.btn-yahoo.active, +.open > .dropdown-toggle.btn-yahoo { + background-image: none; +} +.btn-yahoo .badge { + color: #720e9e; + background-color: #ffffff; +} +/* + * Plugin: Full Calendar + * --------------------- + */ +.fc-button { + background: #f4f4f4; + background-image: none; + color: #444; + border-color: #ddd; + border-bottom-color: #ddd; +} +.fc-button:hover, +.fc-button:active, +.fc-button.hover { + background-color: #e9e9e9; +} +.fc-header-title h2 { + font-size: 15px; + line-height: 1.6em; + color: #666; + margin-left: 10px; +} +.fc-header-right { + padding-right: 10px; +} +.fc-header-left { + padding-left: 10px; +} +.fc-widget-header { + background: #fafafa; +} +.fc-grid { + width: 100%; + border: 0; +} +.fc-widget-header:first-of-type, +.fc-widget-content:first-of-type { + border-left: 0; + border-right: 0; +} +.fc-widget-header:last-of-type, +.fc-widget-content:last-of-type { + border-right: 0; +} +.fc-toolbar { + padding: 10px; + margin: 0; +} +.fc-day-number { + font-size: 20px; + font-weight: 300; + padding-right: 10px; +} +.fc-color-picker { + list-style: none; + margin: 0; + padding: 0; +} +.fc-color-picker > li { + float: left; + font-size: 30px; + margin-right: 5px; + line-height: 30px; +} +.fc-color-picker > li .fa { + -webkit-transition: -webkit-transform linear 0.3s; + -moz-transition: -moz-transform linear 0.3s; + -o-transition: -o-transform linear 0.3s; + transition: transform linear 0.3s; +} +.fc-color-picker > li .fa:hover { + -webkit-transform: rotate(30deg); + -ms-transform: rotate(30deg); + -o-transform: rotate(30deg); + transform: rotate(30deg); +} +#add-new-event { + -webkit-transition: all linear 0.3s; + -o-transition: all linear 0.3s; + transition: all linear 0.3s; +} +.external-event { + padding: 5px 10px; + font-weight: bold; + margin-bottom: 4px; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1); + border-radius: 3px; + cursor: move; +} +.external-event:hover { + box-shadow: inset 0 0 90px rgba(0, 0, 0, 0.2); +} +/* + * Plugin: Select2 + * --------------- + */ +.select2-container--default.select2-container--focus, +.select2-selection.select2-container--focus, +.select2-container--default:focus, +.select2-selection:focus, +.select2-container--default:active, +.select2-selection:active { + outline: none; +} +.select2-container--default .select2-selection--single, +.select2-selection .select2-selection--single { + border: 1px solid #d2d6de; + border-radius: 0; + padding: 6px 12px; + height: 34px; +} +.select2-container--default.select2-container--open { + border-color: #3c8dbc; +} +.select2-dropdown { + border: 1px solid #d2d6de; + border-radius: 0; +} +.select2-container--default .select2-results__option--highlighted[aria-selected] { + background-color: #3c8dbc; + color: white; +} +.select2-results__option { + padding: 6px 12px; + user-select: none; + -webkit-user-select: none; +} +.select2-container .select2-selection--single .select2-selection__rendered { + padding-left: 0; + padding-right: 0; + height: auto; + margin-top: -4px; +} +.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered { + padding-right: 6px; + padding-left: 20px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow { + height: 28px; + right: 3px; +} +.select2-container--default .select2-selection--single .select2-selection__arrow b { + margin-top: 0; +} +.select2-dropdown .select2-search__field, +.select2-search--inline .select2-search__field { + border: 1px solid #d2d6de; +} +.select2-dropdown .select2-search__field:focus, +.select2-search--inline .select2-search__field:focus { + outline: none; + border: 1px solid #3c8dbc; +} +.select2-container--default .select2-results__option[aria-disabled=true] { + color: #999; +} +.select2-container--default .select2-results__option[aria-selected=true] { + background-color: #ddd; +} +.select2-container--default .select2-results__option[aria-selected=true], +.select2-container--default .select2-results__option[aria-selected=true]:hover { + color: #444; +} +.select2-container--default .select2-selection--multiple { + border: 1px solid #d2d6de; + border-radius: 0; +} +.select2-container--default .select2-selection--multiple:focus { + border-color: #3c8dbc; +} +.select2-container--default.select2-container--focus .select2-selection--multiple { + border-color: #d2d6de; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice { + background-color: #3c8dbc; + border-color: #367fa9; + padding: 1px 10px; + color: #fff; +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove { + margin-right: 5px; + color: rgba(255, 255, 255, 0.7); +} +.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover { + color: #fff; +} +.select2-container .select2-selection--single .select2-selection__rendered { + padding-right: 10px; +} +/* + * General: Miscellaneous + * ---------------------- + */ +.pad { + padding: 10px; +} +.margin { + margin: 10px; +} +.margin-bottom { + margin-bottom: 20px; +} +.margin-bottom-none { + margin-bottom: 0; +} +.margin-r-5 { + margin-right: 5px; +} +.inline { + display: inline; +} +.description-block { + display: block; + margin: 10px 0; + text-align: center; +} +.description-block.margin-bottom { + margin-bottom: 25px; +} +.description-block > .description-header { + margin: 0; + padding: 0; + font-weight: 600; + font-size: 16px; +} +.description-block > .description-text { + text-transform: uppercase; +} +.bg-red, +.bg-yellow, +.bg-aqua, +.bg-blue, +.bg-light-blue, +.bg-green, +.bg-navy, +.bg-teal, +.bg-olive, +.bg-lime, +.bg-orange, +.bg-fuchsia, +.bg-purple, +.bg-maroon, +.bg-black, +.bg-red-active, +.bg-yellow-active, +.bg-aqua-active, +.bg-blue-active, +.bg-light-blue-active, +.bg-green-active, +.bg-navy-active, +.bg-teal-active, +.bg-olive-active, +.bg-lime-active, +.bg-orange-active, +.bg-fuchsia-active, +.bg-purple-active, +.bg-maroon-active, +.bg-black-active, +.callout.callout-danger, +.callout.callout-warning, +.callout.callout-info, +.callout.callout-success, +.alert-success, +.alert-danger, +.alert-error, +.alert-warning, +.alert-info, +.label-danger, +.label-info, +.label-warning, +.label-primary, +.label-success, +.modal-primary .modal-body, +.modal-primary .modal-header, +.modal-primary .modal-footer, +.modal-warning .modal-body, +.modal-warning .modal-header, +.modal-warning .modal-footer, +.modal-info .modal-body, +.modal-info .modal-header, +.modal-info .modal-footer, +.modal-success .modal-body, +.modal-success .modal-header, +.modal-success .modal-footer, +.modal-danger .modal-body, +.modal-danger .modal-header, +.modal-danger .modal-footer { + color: #fff !important; +} +.bg-gray { + color: #000; + background-color: #d2d6de !important; +} +.bg-gray-light { + background-color: #f7f7f7; +} +.bg-black { + background-color: #111111 !important; +} +.bg-red, +.callout.callout-danger, +.alert-danger, +.alert-error, +.label-danger, +.modal-danger .modal-body { + background-color: #dd4b39 !important; +} +.bg-yellow, +.callout.callout-warning, +.alert-warning, +.label-warning, +.modal-warning .modal-body { + background-color: #f39c12 !important; +} +.bg-aqua, +.callout.callout-info, +.alert-info, +.label-info, +.modal-info .modal-body { + background-color: #00c0ef !important; +} +.bg-blue { + background-color: #0073b7 !important; +} +.bg-light-blue, +.label-primary, +.modal-primary .modal-body { + background-color: #3c8dbc !important; +} +.bg-green, +.callout.callout-success, +.alert-success, +.label-success, +.modal-success .modal-body { + background-color: #00a65a !important; +} +.bg-navy { + background-color: #001f3f !important; +} +.bg-teal { + background-color: #39cccc !important; +} +.bg-olive { + background-color: #3d9970 !important; +} +.bg-lime { + background-color: #01ff70 !important; +} +.bg-orange { + background-color: #ff851b !important; +} +.bg-fuchsia { + background-color: #f012be !important; +} +.bg-purple { + background-color: #605ca8 !important; +} +.bg-maroon { + background-color: #d81b60 !important; +} +.bg-gray-active { + color: #000; + background-color: #b5bbc8 !important; +} +.bg-black-active { + background-color: #000000 !important; +} +.bg-red-active, +.modal-danger .modal-header, +.modal-danger .modal-footer { + background-color: #d33724 !important; +} +.bg-yellow-active, +.modal-warning .modal-header, +.modal-warning .modal-footer { + background-color: #db8b0b !important; +} +.bg-aqua-active, +.modal-info .modal-header, +.modal-info .modal-footer { + background-color: #00a7d0 !important; +} +.bg-blue-active { + background-color: #005384 !important; +} +.bg-light-blue-active, +.modal-primary .modal-header, +.modal-primary .modal-footer { + background-color: #357ca5 !important; +} +.bg-green-active, +.modal-success .modal-header, +.modal-success .modal-footer { + background-color: #008d4c !important; +} +.bg-navy-active { + background-color: #001a35 !important; +} +.bg-teal-active { + background-color: #30bbbb !important; +} +.bg-olive-active { + background-color: #368763 !important; +} +.bg-lime-active { + background-color: #00e765 !important; +} +.bg-orange-active { + background-color: #ff7701 !important; +} +.bg-fuchsia-active { + background-color: #db0ead !important; +} +.bg-purple-active { + background-color: #555299 !important; +} +.bg-maroon-active { + background-color: #ca195a !important; +} +[class^="bg-"].disabled { + opacity: 0.65; + filter: alpha(opacity=65); +} +.text-red { + color: #dd4b39 !important; +} +.text-yellow { + color: #f39c12 !important; +} +.text-aqua { + color: #00c0ef !important; +} +.text-blue { + color: #0073b7 !important; +} +.text-black { + color: #111111 !important; +} +.text-light-blue { + color: #3c8dbc !important; +} +.text-green { + color: #00a65a !important; +} +.text-gray { + color: #d2d6de !important; +} +.text-navy { + color: #001f3f !important; +} +.text-teal { + color: #39cccc !important; +} +.text-olive { + color: #3d9970 !important; +} +.text-lime { + color: #01ff70 !important; +} +.text-orange { + color: #ff851b !important; +} +.text-fuchsia { + color: #f012be !important; +} +.text-purple { + color: #605ca8 !important; +} +.text-maroon { + color: #d81b60 !important; +} +.link-muted { + color: #7a869d; +} +.link-muted:hover, +.link-muted:focus { + color: #606c84; +} +.link-black { + color: #666; +} +.link-black:hover, +.link-black:focus { + color: #999; +} +.hide { + display: none !important; +} +.no-border { + border: 0 !important; +} +.no-padding { + padding: 0 !important; +} +.no-margin { + margin: 0 !important; +} +.no-shadow { + box-shadow: none !important; +} +.list-unstyled, +.chart-legend, +.contacts-list, +.users-list, +.mailbox-attachments { + list-style: none; + margin: 0; + padding: 0; +} +.list-group-unbordered > .list-group-item { + border-left: 0; + border-right: 0; + border-radius: 0; + padding-left: 0; + padding-right: 0; +} +.flat { + border-radius: 0 !important; +} +.text-bold, +.text-bold.table td, +.text-bold.table th { + font-weight: 700; +} +.text-sm { + font-size: 12px; +} +.jqstooltip { + padding: 5px !important; + width: auto !important; + height: auto !important; +} +.bg-teal-gradient { + background: #39cccc !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important; + background: -ms-linear-gradient(bottom, #39cccc, #7adddd) !important; + background: -moz-linear-gradient(center bottom, #39cccc 0%, #7adddd 100%) !important; + background: -o-linear-gradient(#7adddd, #39cccc) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important; + color: #fff; +} +.bg-light-blue-gradient { + background: #3c8dbc !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #3c8dbc), color-stop(1, #67a8ce)) !important; + background: -ms-linear-gradient(bottom, #3c8dbc, #67a8ce) !important; + background: -moz-linear-gradient(center bottom, #3c8dbc 0%, #67a8ce 100%) !important; + background: -o-linear-gradient(#67a8ce, #3c8dbc) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0) !important; + color: #fff; +} +.bg-blue-gradient { + background: #0073b7 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0073b7), color-stop(1, #0089db)) !important; + background: -ms-linear-gradient(bottom, #0073b7, #0089db) !important; + background: -moz-linear-gradient(center bottom, #0073b7 0%, #0089db 100%) !important; + background: -o-linear-gradient(#0089db, #0073b7) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0) !important; + color: #fff; +} +.bg-aqua-gradient { + background: #00c0ef !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important; + background: -ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important; + background: -moz-linear-gradient(center bottom, #00c0ef 0%, #14d1ff 100%) !important; + background: -o-linear-gradient(#14d1ff, #00c0ef) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important; + color: #fff; +} +.bg-yellow-gradient { + background: #f39c12 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important; + background: -ms-linear-gradient(bottom, #f39c12, #f7bc60) !important; + background: -moz-linear-gradient(center bottom, #f39c12 0%, #f7bc60 100%) !important; + background: -o-linear-gradient(#f7bc60, #f39c12) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important; + color: #fff; +} +.bg-purple-gradient { + background: #605ca8 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important; + background: -ms-linear-gradient(bottom, #605ca8, #9491c4) !important; + background: -moz-linear-gradient(center bottom, #605ca8 0%, #9491c4 100%) !important; + background: -o-linear-gradient(#9491c4, #605ca8) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important; + color: #fff; +} +.bg-green-gradient { + background: #00a65a !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important; + background: -ms-linear-gradient(bottom, #00a65a, #00ca6d) !important; + background: -moz-linear-gradient(center bottom, #00a65a 0%, #00ca6d 100%) !important; + background: -o-linear-gradient(#00ca6d, #00a65a) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important; + color: #fff; +} +.bg-red-gradient { + background: #dd4b39 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important; + background: -ms-linear-gradient(bottom, #dd4b39, #e47365) !important; + background: -moz-linear-gradient(center bottom, #dd4b39 0%, #e47365 100%) !important; + background: -o-linear-gradient(#e47365, #dd4b39) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important; + color: #fff; +} +.bg-black-gradient { + background: #111111 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #111111), color-stop(1, #2b2b2b)) !important; + background: -ms-linear-gradient(bottom, #111111, #2b2b2b) !important; + background: -moz-linear-gradient(center bottom, #111111 0%, #2b2b2b 100%) !important; + background: -o-linear-gradient(#2b2b2b, #111111) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important; + color: #fff; +} +.bg-maroon-gradient { + background: #d81b60 !important; + background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important; + background: -ms-linear-gradient(bottom, #d81b60, #e73f7c) !important; + background: -moz-linear-gradient(center bottom, #d81b60 0%, #e73f7c 100%) !important; + background: -o-linear-gradient(#e73f7c, #d81b60) !important; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important; + color: #fff; +} +.description-block .description-icon { + font-size: 16px; +} +.no-pad-top { + padding-top: 0; +} +.position-static { + position: static !important; +} +.list-header { + font-size: 15px; + padding: 10px 4px; + font-weight: bold; + color: #666; +} +.list-seperator { + height: 1px; + background: #f4f4f4; + margin: 15px 0 9px 0; +} +.list-link > a { + padding: 4px; + color: #777; +} +.list-link > a:hover { + color: #222; +} +.font-light { + font-weight: 300; +} +.user-block:before, +.user-block:after { + content: " "; + display: table; +} +.user-block:after { + clear: both; +} +.user-block img { + width: 40px; + height: 40px; + float: left; +} +.user-block .username, +.user-block .description, +.user-block .comment { + display: block; + margin-left: 50px; +} +.user-block .username { + font-size: 16px; + font-weight: 600; +} +.user-block .description { + color: #999; + font-size: 13px; +} +.user-block.user-block-sm .username, +.user-block.user-block-sm .description, +.user-block.user-block-sm .comment { + margin-left: 40px; +} +.user-block.user-block-sm .username { + font-size: 14px; +} +.img-sm, +.img-md, +.img-lg, +.box-comments .box-comment img, +.user-block.user-block-sm img { + float: left; +} +.img-sm, +.box-comments .box-comment img, +.user-block.user-block-sm img { + width: 30px !important; + height: 30px !important; +} +.img-sm + .img-push { + margin-left: 40px; +} +.img-md { + width: 60px; + height: 60px; +} +.img-md + .img-push { + margin-left: 70px; +} +.img-lg { + width: 100px; + height: 100px; +} +.img-lg + .img-push { + margin-left: 110px; +} +.img-bordered { + border: 3px solid #d2d6de; + padding: 3px; +} +.img-bordered-sm { + border: 2px solid #d2d6de; + padding: 2px; +} +.attachment-block { + border: 1px solid #f4f4f4; + padding: 5px; + margin-bottom: 10px; + background: #f7f7f7; +} +.attachment-block .attachment-img { + max-width: 100px; + max-height: 100px; + height: auto; + float: left; +} +.attachment-block .attachment-pushed { + margin-left: 110px; +} +.attachment-block .attachment-heading { + margin: 0; +} +.attachment-block .attachment-text { + color: #555; +} +.connectedSortable { + min-height: 100px; +} +.ui-helper-hidden-accessible { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; +} +.sort-highlight { + background: #f4f4f4; + border: 1px dashed #ddd; + margin-bottom: 10px; +} +.full-opacity-hover { + opacity: 0.65; + filter: alpha(opacity=65); +} +.full-opacity-hover:hover { + opacity: 1; + filter: alpha(opacity=100); +} +.chart { + position: relative; + overflow: hidden; + width: 100%; +} +.chart svg, +.chart canvas { + width: 100% !important; +} +/* + * Misc: print + * ----------- + */ +@media print { + .no-print, + .main-sidebar, + .left-side, + .main-header, + .content-header { + display: none !important; + } + .content-wrapper, + .right-side, + .main-footer { + margin-left: 0 !important; + min-height: 0 !important; + -webkit-transform: translate(0, 0) !important; + -ms-transform: translate(0, 0) !important; + -o-transform: translate(0, 0) !important; + transform: translate(0, 0) !important; + } + .fixed .content-wrapper, + .fixed .right-side { + padding-top: 0 !important; + } + .invoice { + width: 100%; + border: 0; + margin: 0; + padding: 0; + } + .invoice-col { + float: left; + width: 33.3333333%; + } + .table-responsive { + overflow: auto; + } + .table-responsive > .table tr th, + .table-responsive > .table tr td { + white-space: normal !important; + } +} diff --git a/web/static/dist/css/AdminLTE.min.css b/web/static/dist/css/AdminLTE.min.css new file mode 100644 index 0000000..78ff989 --- /dev/null +++ b/web/static/dist/css/AdminLTE.min.css @@ -0,0 +1,6 @@ + * AdminLTE v2.3.2 + * Author: Almsaeed Studio + * Website: Almsaeed Studio + * License: Open source - MIT + * Please visit http://opensource.org/licenses/MIT for more information +!*/html,body{min-height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{min-height:100%;position:relative;overflow:hidden}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.right-side,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .right-side,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.right-side,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .right-side,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .right-side,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper,.right-side{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}body.hold-transition .content-wrapper,body.hold-transition .right-side,body.hold-transition .main-footer,body.hold-transition .main-sidebar,body.hold-transition .left-side,body.hold-transition .main-header>.navbar,body.hold-transition .main-header .logo{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#3c8dbc}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#72afd2}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header>.navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header>.navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar,.left-side{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar,.left-side{padding-top:100px}}@media (max-width:767px){.main-sidebar,.left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar,.sidebar-collapse .left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar,.sidebar-open .left-side{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-top:3px;margin-right:5px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;margin-top:3px}.sidebar-menu li.active>a>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu li.active>.treeview-menu{display:block}.sidebar-menu .treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.sidebar-menu .treeview-menu .treeview-menu{padding-left:20px}.sidebar-menu .treeview-menu>li{margin:0}.sidebar-menu .treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.sidebar-menu .treeview-menu>li>a>.fa,.sidebar-menu .treeview-menu>li>a>.glyphicon,.sidebar-menu .treeview-menu>li>a>.ion{width:20px}.sidebar-menu .treeview-menu>li>a>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.fa-angle-down{width:auto}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#b8c7ce}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#222d32}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#181f23;color:#b8c7ce}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#141a1d;border-bottom-color:#141a1d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#222d32;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1e282c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b8c7ce}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#3c8dbc;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#3c8dbc}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{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:-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-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{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:-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-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{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:-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-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{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:-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-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{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:-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)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#3c8dbc}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #3c8dbc}.box.box-solid.box-primary>.box-header{color:#fff;background:#3c8dbc;background-color:#3c8dbc}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#3c8dbc}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#3c8dbc;border-color:#367fa9}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#367fa9}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#3c8dbc}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#3c8dbc}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#3c8dbc}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#307095}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none;border:1px solid #3c8dbc}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#0073b7 !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#3c8dbc !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#005384 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#357ca5 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#0073b7 !important}.text-black{color:#111 !important}.text-light-blue{color:#3c8dbc !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#3c8dbc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #3c8dbc), color-stop(1, #67a8ce)) !important;background:-ms-linear-gradient(bottom, #3c8dbc, #67a8ce) !important;background:-moz-linear-gradient(center bottom, #3c8dbc 0, #67a8ce 100%) !important;background:-o-linear-gradient(#67a8ce, #3c8dbc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#0073b7 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #0073b7), color-stop(1, #0089db)) !important;background:-ms-linear-gradient(bottom, #0073b7, #0089db) !important;background:-moz-linear-gradient(center bottom, #0073b7 0, #0089db 100%) !important;background:-o-linear-gradient(#0089db, #0073b7) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} diff --git a/web/static/dist/css/flotconfig.css b/web/static/dist/css/flotconfig.css new file mode 100644 index 0000000..0d50f8e --- /dev/null +++ b/web/static/dist/css/flotconfig.css @@ -0,0 +1,65 @@ +/* FLOT CHART */ +.flot-chart { + display: block; + height: 200px; +} +.widget .flot-chart.dashboard-chart { + display: block; + height: 120px; + margin-top: 40px; +} +.flot-chart.dashboard-chart { + display: block; + height: 180px; + margin-top: 40px; +} +.flot-chart-content { + width: 100%; + height: 100%; +} +.flot-chart-pie-content { + width: 200px; + height: 200px; + margin: auto; +} +.jqstooltip { + position: absolute; + display: block; + left: 0px; + top: 0px; + visibility: hidden; + background: #2b303a; + background-color: rgba(43, 48, 58, 0.8); + color: white; + text-align: left; + white-space: nowrap; + z-index: 10000; + padding: 5px 5px 5px 5px; + min-height: 22px; + border-radius: 3px; +} +.jqsfield { + color: white; + text-align: left; +} +.h-200 { + min-height: 200px; +} +.legendLabel { + padding-left: 5px; +} +.stat-list li:first-child { + margin-top: 0; +} +.stat-list { + list-style: none; + padding: 0; + margin: 0; +} +.stat-percent { + float: right; +} +.stat-list li { + margin-top: 15px; + position: relative; +} diff --git a/web/static/dist/css/modalconfig.css b/web/static/dist/css/modalconfig.css new file mode 100644 index 0000000..57a033f --- /dev/null +++ b/web/static/dist/css/modalconfig.css @@ -0,0 +1,39 @@ +/* MODAL */ +.modal-content { + background-clip: padding-box; + background-color: #FFFFFF; + border: 1px solid rgba(0, 0, 0, 0); + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + outline: 0 none; + position: relative; +} +.modal-dialog { + z-index: 2200; +} +.modal-body { + padding: 20px 30px 30px 30px; +} +.inmodal .modal-body { + background: #f8fafb; +} +.inmodal .modal-header { + padding: 30px 15px; + text-align: center; +} +.animated.modal.fade .modal-dialog { + -webkit-transform: none; + -ms-transform: none; + -o-transform: none; + transform: none; +} +.inmodal .modal-title { + font-size: 26px; +} +.inmodal .modal-icon { + font-size: 84px; + color: #e2e3e3; +} +.modal-footer { + margin-top: 0; +} diff --git a/web/static/dist/css/skins/_all-skins.css b/web/static/dist/css/skins/_all-skins.css new file mode 100644 index 0000000..ed24088 --- /dev/null +++ b/web/static/dist/css/skins/_all-skins.css @@ -0,0 +1,1806 @@ +/* + * Skin: Blue + * ---------- + */ +.skin-blue .main-header .navbar { + background-color: #3c8dbc; +} +.skin-blue .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-blue .main-header .navbar .nav > li > a:hover, +.skin-blue .main-header .navbar .nav > li > a:active, +.skin-blue .main-header .navbar .nav > li > a:focus, +.skin-blue .main-header .navbar .nav .open > a, +.skin-blue .main-header .navbar .nav .open > a:hover, +.skin-blue .main-header .navbar .nav .open > a:focus, +.skin-blue .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + background-color: #367fa9; +} +@media (max-width: 767px) { + .skin-blue .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-blue .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-blue .main-header .navbar .dropdown-menu li a:hover { + background: #367fa9; + } +} +.skin-blue .main-header .logo { + background-color: #367fa9; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue .main-header .logo:hover { + background-color: #357ca5; +} +.skin-blue .main-header li.user-header { + background-color: #3c8dbc; +} +.skin-blue .content-header { + background: transparent; +} +.skin-blue .wrapper, +.skin-blue .main-sidebar, +.skin-blue .left-side { + background-color: #222d32; +} +.skin-blue .user-panel > .info, +.skin-blue .user-panel > .info > a { + color: #fff; +} +.skin-blue .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-blue .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-blue .sidebar-menu > li:hover > a, +.skin-blue .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #3c8dbc; +} +.skin-blue .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-blue .sidebar a { + color: #b8c7ce; +} +.skin-blue .sidebar a:hover { + text-decoration: none; +} +.skin-blue .treeview-menu > li > a { + color: #8aa4af; +} +.skin-blue .treeview-menu > li.active > a, +.skin-blue .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-blue .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-blue .sidebar-form input[type="text"], +.skin-blue .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-blue .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-blue .sidebar-form input[type="text"]:focus, +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-blue .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +.skin-blue.layout-top-nav .main-header > .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue.layout-top-nav .main-header > .logo:hover { + background-color: #3b8ab8; +} +/* + * Skin: Blue + * ---------- + */ +.skin-blue-light .main-header .navbar { + background-color: #3c8dbc; +} +.skin-blue-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-blue-light .main-header .navbar .nav > li > a:hover, +.skin-blue-light .main-header .navbar .nav > li > a:active, +.skin-blue-light .main-header .navbar .nav > li > a:focus, +.skin-blue-light .main-header .navbar .nav .open > a, +.skin-blue-light .main-header .navbar .nav .open > a:hover, +.skin-blue-light .main-header .navbar .nav .open > a:focus, +.skin-blue-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-blue-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-blue-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-blue-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-blue-light .main-header .navbar .sidebar-toggle:hover { + background-color: #367fa9; +} +@media (max-width: 767px) { + .skin-blue-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-blue-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-blue-light .main-header .navbar .dropdown-menu li a:hover { + background: #367fa9; + } +} +.skin-blue-light .main-header .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue-light .main-header .logo:hover { + background-color: #3b8ab8; +} +.skin-blue-light .main-header li.user-header { + background-color: #3c8dbc; +} +.skin-blue-light .content-header { + background: transparent; +} +.skin-blue-light .wrapper, +.skin-blue-light .main-sidebar, +.skin-blue-light .left-side { + background-color: #f9fafc; +} +.skin-blue-light .content-wrapper, +.skin-blue-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-blue-light .user-panel > .info, +.skin-blue-light .user-panel > .info > a { + color: #444444; +} +.skin-blue-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-blue-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-blue-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-blue-light .sidebar-menu > li:hover > a, +.skin-blue-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-blue-light .sidebar-menu > li.active { + border-left-color: #3c8dbc; +} +.skin-blue-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-blue-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-blue-light .sidebar a { + color: #444444; +} +.skin-blue-light .sidebar a:hover { + text-decoration: none; +} +.skin-blue-light .treeview-menu > li > a { + color: #777777; +} +.skin-blue-light .treeview-menu > li.active > a, +.skin-blue-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-blue-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-blue-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-blue-light .sidebar-form input[type="text"], +.skin-blue-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-blue-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-blue-light .sidebar-form input[type="text"]:focus, +.skin-blue-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-blue-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-blue-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-blue-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +.skin-blue-light .main-footer { + border-top-color: #d2d6de; +} +.skin-blue.layout-top-nav .main-header > .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue.layout-top-nav .main-header > .logo:hover { + background-color: #3b8ab8; +} +/* + * Skin: Black + * ----------- + */ +/* skin-black navbar */ +.skin-black .main-header { + -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); +} +.skin-black .main-header .navbar-toggle { + color: #333; +} +.skin-black .main-header .navbar-brand { + color: #333; + border-right: 1px solid #eee; +} +.skin-black .main-header > .navbar { + background-color: #ffffff; +} +.skin-black .main-header > .navbar .nav > li > a { + color: #333333; +} +.skin-black .main-header > .navbar .nav > li > a:hover, +.skin-black .main-header > .navbar .nav > li > a:active, +.skin-black .main-header > .navbar .nav > li > a:focus, +.skin-black .main-header > .navbar .nav .open > a, +.skin-black .main-header > .navbar .nav .open > a:hover, +.skin-black .main-header > .navbar .nav .open > a:focus, +.skin-black .main-header > .navbar .nav > .active > a { + background: #ffffff; + color: #999999; +} +.skin-black .main-header > .navbar .sidebar-toggle { + color: #333333; +} +.skin-black .main-header > .navbar .sidebar-toggle:hover { + color: #999999; + background: #ffffff; +} +.skin-black .main-header > .navbar > .sidebar-toggle { + color: #333; + border-right: 1px solid #eee; +} +.skin-black .main-header > .navbar .navbar-nav > li > a { + border-right: 1px solid #eee; +} +.skin-black .main-header > .navbar .navbar-custom-menu .navbar-nav > li > a, +.skin-black .main-header > .navbar .navbar-right > li > a { + border-left: 1px solid #eee; + border-right-width: 0; +} +.skin-black .main-header > .logo { + background-color: #ffffff; + color: #333333; + border-bottom: 0 solid transparent; + border-right: 1px solid #eee; +} +.skin-black .main-header > .logo:hover { + background-color: #fcfcfc; +} +@media (max-width: 767px) { + .skin-black .main-header > .logo { + background-color: #222222; + color: #ffffff; + border-bottom: 0 solid transparent; + border-right: none; + } + .skin-black .main-header > .logo:hover { + background-color: #1f1f1f; + } +} +.skin-black .main-header li.user-header { + background-color: #222; +} +.skin-black .content-header { + background: transparent; + box-shadow: none; +} +.skin-black .wrapper, +.skin-black .main-sidebar, +.skin-black .left-side { + background-color: #222d32; +} +.skin-black .user-panel > .info, +.skin-black .user-panel > .info > a { + color: #fff; +} +.skin-black .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-black .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-black .sidebar-menu > li:hover > a, +.skin-black .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #ffffff; +} +.skin-black .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-black .sidebar a { + color: #b8c7ce; +} +.skin-black .sidebar a:hover { + text-decoration: none; +} +.skin-black .treeview-menu > li > a { + color: #8aa4af; +} +.skin-black .treeview-menu > li.active > a, +.skin-black .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-black .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-black .sidebar-form input[type="text"], +.skin-black .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-black .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-black .sidebar-form input[type="text"]:focus, +.skin-black .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-black .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-black .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +.skin-black .pace .pace-progress { + background: #222; +} +.skin-black .pace .pace-activity { + border-top-color: #222; + border-left-color: #222; +} +/* + * Skin: Black + * ----------- + */ +/* skin-black navbar */ +.skin-black-light .main-header { + -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05); +} +.skin-black-light .main-header .navbar-toggle { + color: #333; +} +.skin-black-light .main-header .navbar-brand { + color: #333; + border-right: 1px solid #eee; +} +.skin-black-light .main-header > .navbar { + background-color: #ffffff; +} +.skin-black-light .main-header > .navbar .nav > li > a { + color: #333333; +} +.skin-black-light .main-header > .navbar .nav > li > a:hover, +.skin-black-light .main-header > .navbar .nav > li > a:active, +.skin-black-light .main-header > .navbar .nav > li > a:focus, +.skin-black-light .main-header > .navbar .nav .open > a, +.skin-black-light .main-header > .navbar .nav .open > a:hover, +.skin-black-light .main-header > .navbar .nav .open > a:focus, +.skin-black-light .main-header > .navbar .nav > .active > a { + background: #ffffff; + color: #999999; +} +.skin-black-light .main-header > .navbar .sidebar-toggle { + color: #333333; +} +.skin-black-light .main-header > .navbar .sidebar-toggle:hover { + color: #999999; + background: #ffffff; +} +.skin-black-light .main-header > .navbar > .sidebar-toggle { + color: #333; + border-right: 1px solid #eee; +} +.skin-black-light .main-header > .navbar .navbar-nav > li > a { + border-right: 1px solid #eee; +} +.skin-black-light .main-header > .navbar .navbar-custom-menu .navbar-nav > li > a, +.skin-black-light .main-header > .navbar .navbar-right > li > a { + border-left: 1px solid #eee; + border-right-width: 0; +} +.skin-black-light .main-header > .logo { + background-color: #ffffff; + color: #333333; + border-bottom: 0 solid transparent; + border-right: 1px solid #eee; +} +.skin-black-light .main-header > .logo:hover { + background-color: #fcfcfc; +} +@media (max-width: 767px) { + .skin-black-light .main-header > .logo { + background-color: #222222; + color: #ffffff; + border-bottom: 0 solid transparent; + border-right: none; + } + .skin-black-light .main-header > .logo:hover { + background-color: #1f1f1f; + } +} +.skin-black-light .main-header li.user-header { + background-color: #222; +} +.skin-black-light .content-header { + background: transparent; + box-shadow: none; +} +.skin-black-light .wrapper, +.skin-black-light .main-sidebar, +.skin-black-light .left-side { + background-color: #f9fafc; +} +.skin-black-light .content-wrapper, +.skin-black-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-black-light .user-panel > .info, +.skin-black-light .user-panel > .info > a { + color: #444444; +} +.skin-black-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-black-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-black-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-black-light .sidebar-menu > li:hover > a, +.skin-black-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-black-light .sidebar-menu > li.active { + border-left-color: #ffffff; +} +.skin-black-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-black-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-black-light .sidebar a { + color: #444444; +} +.skin-black-light .sidebar a:hover { + text-decoration: none; +} +.skin-black-light .treeview-menu > li > a { + color: #777777; +} +.skin-black-light .treeview-menu > li.active > a, +.skin-black-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-black-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-black-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-black-light .sidebar-form input[type="text"], +.skin-black-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-black-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-black-light .sidebar-form input[type="text"]:focus, +.skin-black-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-black-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-black-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Green + * ----------- + */ +.skin-green .main-header .navbar { + background-color: #00a65a; +} +.skin-green .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-green .main-header .navbar .nav > li > a:hover, +.skin-green .main-header .navbar .nav > li > a:active, +.skin-green .main-header .navbar .nav > li > a:focus, +.skin-green .main-header .navbar .nav .open > a, +.skin-green .main-header .navbar .nav .open > a:hover, +.skin-green .main-header .navbar .nav .open > a:focus, +.skin-green .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-green .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-green .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-green .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-green .main-header .navbar .sidebar-toggle:hover { + background-color: #008d4c; +} +@media (max-width: 767px) { + .skin-green .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-green .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-green .main-header .navbar .dropdown-menu li a:hover { + background: #008d4c; + } +} +.skin-green .main-header .logo { + background-color: #008d4c; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-green .main-header .logo:hover { + background-color: #008749; +} +.skin-green .main-header li.user-header { + background-color: #00a65a; +} +.skin-green .content-header { + background: transparent; +} +.skin-green .wrapper, +.skin-green .main-sidebar, +.skin-green .left-side { + background-color: #222d32; +} +.skin-green .user-panel > .info, +.skin-green .user-panel > .info > a { + color: #fff; +} +.skin-green .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-green .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-green .sidebar-menu > li:hover > a, +.skin-green .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #00a65a; +} +.skin-green .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-green .sidebar a { + color: #b8c7ce; +} +.skin-green .sidebar a:hover { + text-decoration: none; +} +.skin-green .treeview-menu > li > a { + color: #8aa4af; +} +.skin-green .treeview-menu > li.active > a, +.skin-green .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-green .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-green .sidebar-form input[type="text"], +.skin-green .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-green .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-green .sidebar-form input[type="text"]:focus, +.skin-green .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-green .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-green .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Green + * ----------- + */ +.skin-green-light .main-header .navbar { + background-color: #00a65a; +} +.skin-green-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-green-light .main-header .navbar .nav > li > a:hover, +.skin-green-light .main-header .navbar .nav > li > a:active, +.skin-green-light .main-header .navbar .nav > li > a:focus, +.skin-green-light .main-header .navbar .nav .open > a, +.skin-green-light .main-header .navbar .nav .open > a:hover, +.skin-green-light .main-header .navbar .nav .open > a:focus, +.skin-green-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-green-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-green-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-green-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-green-light .main-header .navbar .sidebar-toggle:hover { + background-color: #008d4c; +} +@media (max-width: 767px) { + .skin-green-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-green-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-green-light .main-header .navbar .dropdown-menu li a:hover { + background: #008d4c; + } +} +.skin-green-light .main-header .logo { + background-color: #00a65a; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-green-light .main-header .logo:hover { + background-color: #00a157; +} +.skin-green-light .main-header li.user-header { + background-color: #00a65a; +} +.skin-green-light .content-header { + background: transparent; +} +.skin-green-light .wrapper, +.skin-green-light .main-sidebar, +.skin-green-light .left-side { + background-color: #f9fafc; +} +.skin-green-light .content-wrapper, +.skin-green-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-green-light .user-panel > .info, +.skin-green-light .user-panel > .info > a { + color: #444444; +} +.skin-green-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-green-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-green-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-green-light .sidebar-menu > li:hover > a, +.skin-green-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-green-light .sidebar-menu > li.active { + border-left-color: #00a65a; +} +.skin-green-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-green-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-green-light .sidebar a { + color: #444444; +} +.skin-green-light .sidebar a:hover { + text-decoration: none; +} +.skin-green-light .treeview-menu > li > a { + color: #777777; +} +.skin-green-light .treeview-menu > li.active > a, +.skin-green-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-green-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-green-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-green-light .sidebar-form input[type="text"], +.skin-green-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-green-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-green-light .sidebar-form input[type="text"]:focus, +.skin-green-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-green-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-green-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-green-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Red + * --------- + */ +.skin-red .main-header .navbar { + background-color: #dd4b39; +} +.skin-red .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-red .main-header .navbar .nav > li > a:hover, +.skin-red .main-header .navbar .nav > li > a:active, +.skin-red .main-header .navbar .nav > li > a:focus, +.skin-red .main-header .navbar .nav .open > a, +.skin-red .main-header .navbar .nav .open > a:hover, +.skin-red .main-header .navbar .nav .open > a:focus, +.skin-red .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-red .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-red .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-red .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-red .main-header .navbar .sidebar-toggle:hover { + background-color: #d73925; +} +@media (max-width: 767px) { + .skin-red .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-red .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-red .main-header .navbar .dropdown-menu li a:hover { + background: #d73925; + } +} +.skin-red .main-header .logo { + background-color: #d73925; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-red .main-header .logo:hover { + background-color: #d33724; +} +.skin-red .main-header li.user-header { + background-color: #dd4b39; +} +.skin-red .content-header { + background: transparent; +} +.skin-red .wrapper, +.skin-red .main-sidebar, +.skin-red .left-side { + background-color: #222d32; +} +.skin-red .user-panel > .info, +.skin-red .user-panel > .info > a { + color: #fff; +} +.skin-red .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-red .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-red .sidebar-menu > li:hover > a, +.skin-red .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #dd4b39; +} +.skin-red .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-red .sidebar a { + color: #b8c7ce; +} +.skin-red .sidebar a:hover { + text-decoration: none; +} +.skin-red .treeview-menu > li > a { + color: #8aa4af; +} +.skin-red .treeview-menu > li.active > a, +.skin-red .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-red .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-red .sidebar-form input[type="text"], +.skin-red .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-red .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-red .sidebar-form input[type="text"]:focus, +.skin-red .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-red .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-red .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Red + * --------- + */ +.skin-red-light .main-header .navbar { + background-color: #dd4b39; +} +.skin-red-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-red-light .main-header .navbar .nav > li > a:hover, +.skin-red-light .main-header .navbar .nav > li > a:active, +.skin-red-light .main-header .navbar .nav > li > a:focus, +.skin-red-light .main-header .navbar .nav .open > a, +.skin-red-light .main-header .navbar .nav .open > a:hover, +.skin-red-light .main-header .navbar .nav .open > a:focus, +.skin-red-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-red-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-red-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-red-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-red-light .main-header .navbar .sidebar-toggle:hover { + background-color: #d73925; +} +@media (max-width: 767px) { + .skin-red-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-red-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-red-light .main-header .navbar .dropdown-menu li a:hover { + background: #d73925; + } +} +.skin-red-light .main-header .logo { + background-color: #dd4b39; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-red-light .main-header .logo:hover { + background-color: #dc4735; +} +.skin-red-light .main-header li.user-header { + background-color: #dd4b39; +} +.skin-red-light .content-header { + background: transparent; +} +.skin-red-light .wrapper, +.skin-red-light .main-sidebar, +.skin-red-light .left-side { + background-color: #f9fafc; +} +.skin-red-light .content-wrapper, +.skin-red-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-red-light .user-panel > .info, +.skin-red-light .user-panel > .info > a { + color: #444444; +} +.skin-red-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-red-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-red-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-red-light .sidebar-menu > li:hover > a, +.skin-red-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-red-light .sidebar-menu > li.active { + border-left-color: #dd4b39; +} +.skin-red-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-red-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-red-light .sidebar a { + color: #444444; +} +.skin-red-light .sidebar a:hover { + text-decoration: none; +} +.skin-red-light .treeview-menu > li > a { + color: #777777; +} +.skin-red-light .treeview-menu > li.active > a, +.skin-red-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-red-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-red-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-red-light .sidebar-form input[type="text"], +.skin-red-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-red-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-red-light .sidebar-form input[type="text"]:focus, +.skin-red-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-red-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-red-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-red-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Yellow + * ------------ + */ +.skin-yellow .main-header .navbar { + background-color: #f39c12; +} +.skin-yellow .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-yellow .main-header .navbar .nav > li > a:hover, +.skin-yellow .main-header .navbar .nav > li > a:active, +.skin-yellow .main-header .navbar .nav > li > a:focus, +.skin-yellow .main-header .navbar .nav .open > a, +.skin-yellow .main-header .navbar .nav .open > a:hover, +.skin-yellow .main-header .navbar .nav .open > a:focus, +.skin-yellow .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-yellow .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-yellow .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-yellow .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-yellow .main-header .navbar .sidebar-toggle:hover { + background-color: #e08e0b; +} +@media (max-width: 767px) { + .skin-yellow .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-yellow .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-yellow .main-header .navbar .dropdown-menu li a:hover { + background: #e08e0b; + } +} +.skin-yellow .main-header .logo { + background-color: #e08e0b; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-yellow .main-header .logo:hover { + background-color: #db8b0b; +} +.skin-yellow .main-header li.user-header { + background-color: #f39c12; +} +.skin-yellow .content-header { + background: transparent; +} +.skin-yellow .wrapper, +.skin-yellow .main-sidebar, +.skin-yellow .left-side { + background-color: #222d32; +} +.skin-yellow .user-panel > .info, +.skin-yellow .user-panel > .info > a { + color: #fff; +} +.skin-yellow .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-yellow .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-yellow .sidebar-menu > li:hover > a, +.skin-yellow .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #f39c12; +} +.skin-yellow .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-yellow .sidebar a { + color: #b8c7ce; +} +.skin-yellow .sidebar a:hover { + text-decoration: none; +} +.skin-yellow .treeview-menu > li > a { + color: #8aa4af; +} +.skin-yellow .treeview-menu > li.active > a, +.skin-yellow .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-yellow .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-yellow .sidebar-form input[type="text"], +.skin-yellow .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-yellow .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-yellow .sidebar-form input[type="text"]:focus, +.skin-yellow .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-yellow .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-yellow .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Yellow + * ------------ + */ +.skin-yellow-light .main-header .navbar { + background-color: #f39c12; +} +.skin-yellow-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-yellow-light .main-header .navbar .nav > li > a:hover, +.skin-yellow-light .main-header .navbar .nav > li > a:active, +.skin-yellow-light .main-header .navbar .nav > li > a:focus, +.skin-yellow-light .main-header .navbar .nav .open > a, +.skin-yellow-light .main-header .navbar .nav .open > a:hover, +.skin-yellow-light .main-header .navbar .nav .open > a:focus, +.skin-yellow-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-yellow-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-yellow-light .main-header .navbar .sidebar-toggle:hover { + background-color: #e08e0b; +} +@media (max-width: 767px) { + .skin-yellow-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-yellow-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-yellow-light .main-header .navbar .dropdown-menu li a:hover { + background: #e08e0b; + } +} +.skin-yellow-light .main-header .logo { + background-color: #f39c12; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-yellow-light .main-header .logo:hover { + background-color: #f39a0d; +} +.skin-yellow-light .main-header li.user-header { + background-color: #f39c12; +} +.skin-yellow-light .content-header { + background: transparent; +} +.skin-yellow-light .wrapper, +.skin-yellow-light .main-sidebar, +.skin-yellow-light .left-side { + background-color: #f9fafc; +} +.skin-yellow-light .content-wrapper, +.skin-yellow-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-yellow-light .user-panel > .info, +.skin-yellow-light .user-panel > .info > a { + color: #444444; +} +.skin-yellow-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-yellow-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-yellow-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-yellow-light .sidebar-menu > li:hover > a, +.skin-yellow-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-yellow-light .sidebar-menu > li.active { + border-left-color: #f39c12; +} +.skin-yellow-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-yellow-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-yellow-light .sidebar a { + color: #444444; +} +.skin-yellow-light .sidebar a:hover { + text-decoration: none; +} +.skin-yellow-light .treeview-menu > li > a { + color: #777777; +} +.skin-yellow-light .treeview-menu > li.active > a, +.skin-yellow-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-yellow-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-yellow-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-yellow-light .sidebar-form input[type="text"], +.skin-yellow-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-yellow-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-yellow-light .sidebar-form input[type="text"]:focus, +.skin-yellow-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-yellow-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-yellow-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-yellow-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} +/* + * Skin: Purple + * ------------ + */ +.skin-purple .main-header .navbar { + background-color: #605ca8; +} +.skin-purple .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-purple .main-header .navbar .nav > li > a:hover, +.skin-purple .main-header .navbar .nav > li > a:active, +.skin-purple .main-header .navbar .nav > li > a:focus, +.skin-purple .main-header .navbar .nav .open > a, +.skin-purple .main-header .navbar .nav .open > a:hover, +.skin-purple .main-header .navbar .nav .open > a:focus, +.skin-purple .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-purple .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-purple .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-purple .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-purple .main-header .navbar .sidebar-toggle:hover { + background-color: #555299; +} +@media (max-width: 767px) { + .skin-purple .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-purple .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-purple .main-header .navbar .dropdown-menu li a:hover { + background: #555299; + } +} +.skin-purple .main-header .logo { + background-color: #555299; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-purple .main-header .logo:hover { + background-color: #545096; +} +.skin-purple .main-header li.user-header { + background-color: #605ca8; +} +.skin-purple .content-header { + background: transparent; +} +.skin-purple .wrapper, +.skin-purple .main-sidebar, +.skin-purple .left-side { + background-color: #222d32; +} +.skin-purple .user-panel > .info, +.skin-purple .user-panel > .info > a { + color: #fff; +} +.skin-purple .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-purple .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-purple .sidebar-menu > li:hover > a, +.skin-purple .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #605ca8; +} +.skin-purple .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-purple .sidebar a { + color: #b8c7ce; +} +.skin-purple .sidebar a:hover { + text-decoration: none; +} +.skin-purple .treeview-menu > li > a { + color: #8aa4af; +} +.skin-purple .treeview-menu > li.active > a, +.skin-purple .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-purple .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-purple .sidebar-form input[type="text"], +.skin-purple .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-purple .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-purple .sidebar-form input[type="text"]:focus, +.skin-purple .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-purple .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-purple .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +/* + * Skin: Purple + * ------------ + */ +.skin-purple-light .main-header .navbar { + background-color: #605ca8; +} +.skin-purple-light .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-purple-light .main-header .navbar .nav > li > a:hover, +.skin-purple-light .main-header .navbar .nav > li > a:active, +.skin-purple-light .main-header .navbar .nav > li > a:focus, +.skin-purple-light .main-header .navbar .nav .open > a, +.skin-purple-light .main-header .navbar .nav .open > a:hover, +.skin-purple-light .main-header .navbar .nav .open > a:focus, +.skin-purple-light .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-purple-light .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-purple-light .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-purple-light .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-purple-light .main-header .navbar .sidebar-toggle:hover { + background-color: #555299; +} +@media (max-width: 767px) { + .skin-purple-light .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-purple-light .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-purple-light .main-header .navbar .dropdown-menu li a:hover { + background: #555299; + } +} +.skin-purple-light .main-header .logo { + background-color: #605ca8; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-purple-light .main-header .logo:hover { + background-color: #5d59a6; +} +.skin-purple-light .main-header li.user-header { + background-color: #605ca8; +} +.skin-purple-light .content-header { + background: transparent; +} +.skin-purple-light .wrapper, +.skin-purple-light .main-sidebar, +.skin-purple-light .left-side { + background-color: #f9fafc; +} +.skin-purple-light .content-wrapper, +.skin-purple-light .main-footer { + border-left: 1px solid #d2d6de; +} +.skin-purple-light .user-panel > .info, +.skin-purple-light .user-panel > .info > a { + color: #444444; +} +.skin-purple-light .sidebar-menu > li { + -webkit-transition: border-left-color 0.3s ease; + -o-transition: border-left-color 0.3s ease; + transition: border-left-color 0.3s ease; +} +.skin-purple-light .sidebar-menu > li.header { + color: #848484; + background: #f9fafc; +} +.skin-purple-light .sidebar-menu > li > a { + border-left: 3px solid transparent; + font-weight: 600; +} +.skin-purple-light .sidebar-menu > li:hover > a, +.skin-purple-light .sidebar-menu > li.active > a { + color: #000000; + background: #f4f4f5; +} +.skin-purple-light .sidebar-menu > li.active { + border-left-color: #605ca8; +} +.skin-purple-light .sidebar-menu > li.active > a { + font-weight: 600; +} +.skin-purple-light .sidebar-menu > li > .treeview-menu { + background: #f4f4f5; +} +.skin-purple-light .sidebar a { + color: #444444; +} +.skin-purple-light .sidebar a:hover { + text-decoration: none; +} +.skin-purple-light .treeview-menu > li > a { + color: #777777; +} +.skin-purple-light .treeview-menu > li.active > a, +.skin-purple-light .treeview-menu > li > a:hover { + color: #000000; +} +.skin-purple-light .treeview-menu > li.active > a { + font-weight: 600; +} +.skin-purple-light .sidebar-form { + border-radius: 3px; + border: 1px solid #d2d6de; + margin: 10px 10px; +} +.skin-purple-light .sidebar-form input[type="text"], +.skin-purple-light .sidebar-form .btn { + box-shadow: none; + background-color: #fff; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-purple-light .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-purple-light .sidebar-form input[type="text"]:focus, +.skin-purple-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-purple-light .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-purple-light .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +@media (min-width: 768px) { + .skin-purple-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu { + border-left: 1px solid #d2d6de; + } +} diff --git a/web/static/dist/css/skins/_all-skins.min.css b/web/static/dist/css/skins/_all-skins.min.css new file mode 100644 index 0000000..7b420a5 --- /dev/null +++ b/web/static/dist/css/skins/_all-skins.min.css @@ -0,0 +1 @@ +.skin-blue .main-header .navbar{background-color:#3c8dbc}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-blue .main-header .logo{background-color:#367fa9;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#357ca5}.skin-blue .main-header li.user-header{background-color:#3c8dbc}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#222d32}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#3c8dbc}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-blue .sidebar a{color:#b8c7ce}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .treeview-menu>li>a{color:#8aa4af}.skin-blue .treeview-menu>li.active>a,.skin-blue .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8}.skin-blue-light .main-header .navbar{background-color:#3c8dbc}.skin-blue-light .main-header .navbar .nav>li>a{color:#fff}.skin-blue-light .main-header .navbar .nav>li>a:hover,.skin-blue-light .main-header .navbar .nav>li>a:active,.skin-blue-light .main-header .navbar .nav>li>a:focus,.skin-blue-light .main-header .navbar .nav .open>a,.skin-blue-light .main-header .navbar .nav .open>a:hover,.skin-blue-light .main-header .navbar .nav .open>a:focus,.skin-blue-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue-light .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-blue-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue-light .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-blue-light .main-header .logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue-light .main-header .logo:hover{background-color:#3b8ab8}.skin-blue-light .main-header li.user-header{background-color:#3c8dbc}.skin-blue-light .content-header{background:transparent}.skin-blue-light .wrapper,.skin-blue-light .main-sidebar,.skin-blue-light .left-side{background-color:#f9fafc}.skin-blue-light .content-wrapper,.skin-blue-light .main-footer{border-left:1px solid #d2d6de}.skin-blue-light .user-panel>.info,.skin-blue-light .user-panel>.info>a{color:#444}.skin-blue-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-blue-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-blue-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-blue-light .sidebar-menu>li:hover>a,.skin-blue-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-blue-light .sidebar-menu>li.active{border-left-color:#3c8dbc}.skin-blue-light .sidebar-menu>li.active>a{font-weight:600}.skin-blue-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-blue-light .sidebar a{color:#444}.skin-blue-light .sidebar a:hover{text-decoration:none}.skin-blue-light .treeview-menu>li>a{color:#777}.skin-blue-light .treeview-menu>li.active>a,.skin-blue-light .treeview-menu>li>a:hover{color:#000}.skin-blue-light .treeview-menu>li.active>a{font-weight:600}.skin-blue-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-blue-light .sidebar-form input[type="text"],.skin-blue-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-blue-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue-light .sidebar-form input[type="text"]:focus,.skin-blue-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-blue-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-blue-light .main-footer{border-top-color:#d2d6de}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8}.skin-black .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.skin-black .main-header .navbar-toggle{color:#333}.skin-black .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black .main-header>.navbar{background-color:#fff}.skin-black .main-header>.navbar .nav>li>a{color:#333}.skin-black .main-header>.navbar .nav>li>a:hover,.skin-black .main-header>.navbar .nav>li>a:active,.skin-black .main-header>.navbar .nav>li>a:focus,.skin-black .main-header>.navbar .nav .open>a,.skin-black .main-header>.navbar .nav .open>a:hover,.skin-black .main-header>.navbar .nav .open>a:focus,.skin-black .main-header>.navbar .nav>.active>a{background:#fff;color:#999}.skin-black .main-header>.navbar .sidebar-toggle{color:#333}.skin-black .main-header>.navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black .main-header>.navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black .main-header>.navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black .main-header>.navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black .main-header>.navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black .main-header li.user-header{background-color:#222}.skin-black .content-header{background:transparent;box-shadow:none}.skin-black .wrapper,.skin-black .main-sidebar,.skin-black .left-side{background-color:#222d32}.skin-black .user-panel>.info,.skin-black .user-panel>.info>a{color:#fff}.skin-black .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-black .sidebar-menu>li>a{border-left:3px solid transparent}.skin-black .sidebar-menu>li:hover>a,.skin-black .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#fff}.skin-black .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-black .sidebar a{color:#b8c7ce}.skin-black .sidebar a:hover{text-decoration:none}.skin-black .treeview-menu>li>a{color:#8aa4af}.skin-black .treeview-menu>li.active>a,.skin-black .treeview-menu>li>a:hover{color:#fff}.skin-black .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-black .sidebar-form input[type="text"],.skin-black .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-black .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black .sidebar-form input[type="text"]:focus,.skin-black .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-black .pace .pace-progress{background:#222}.skin-black .pace .pace-activity{border-top-color:#222;border-left-color:#222}.skin-black-light .main-header{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.skin-black-light .main-header .navbar-toggle{color:#333}.skin-black-light .main-header .navbar-brand{color:#333;border-right:1px solid #eee}.skin-black-light .main-header>.navbar{background-color:#fff}.skin-black-light .main-header>.navbar .nav>li>a{color:#333}.skin-black-light .main-header>.navbar .nav>li>a:hover,.skin-black-light .main-header>.navbar .nav>li>a:active,.skin-black-light .main-header>.navbar .nav>li>a:focus,.skin-black-light .main-header>.navbar .nav .open>a,.skin-black-light .main-header>.navbar .nav .open>a:hover,.skin-black-light .main-header>.navbar .nav .open>a:focus,.skin-black-light .main-header>.navbar .nav>.active>a{background:#fff;color:#999}.skin-black-light .main-header>.navbar .sidebar-toggle{color:#333}.skin-black-light .main-header>.navbar .sidebar-toggle:hover{color:#999;background:#fff}.skin-black-light .main-header>.navbar>.sidebar-toggle{color:#333;border-right:1px solid #eee}.skin-black-light .main-header>.navbar .navbar-nav>li>a{border-right:1px solid #eee}.skin-black-light .main-header>.navbar .navbar-custom-menu .navbar-nav>li>a,.skin-black-light .main-header>.navbar .navbar-right>li>a{border-left:1px solid #eee;border-right-width:0}.skin-black-light .main-header>.logo{background-color:#fff;color:#333;border-bottom:0 solid transparent;border-right:1px solid #eee}.skin-black-light .main-header>.logo:hover{background-color:#fcfcfc}@media (max-width:767px){.skin-black-light .main-header>.logo{background-color:#222;color:#fff;border-bottom:0 solid transparent;border-right:none}.skin-black-light .main-header>.logo:hover{background-color:#1f1f1f}}.skin-black-light .main-header li.user-header{background-color:#222}.skin-black-light .content-header{background:transparent;box-shadow:none}.skin-black-light .wrapper,.skin-black-light .main-sidebar,.skin-black-light .left-side{background-color:#f9fafc}.skin-black-light .content-wrapper,.skin-black-light .main-footer{border-left:1px solid #d2d6de}.skin-black-light .user-panel>.info,.skin-black-light .user-panel>.info>a{color:#444}.skin-black-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-black-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-black-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-black-light .sidebar-menu>li:hover>a,.skin-black-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-black-light .sidebar-menu>li.active{border-left-color:#fff}.skin-black-light .sidebar-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-black-light .sidebar a{color:#444}.skin-black-light .sidebar a:hover{text-decoration:none}.skin-black-light .treeview-menu>li>a{color:#777}.skin-black-light .treeview-menu>li.active>a,.skin-black-light .treeview-menu>li>a:hover{color:#000}.skin-black-light .treeview-menu>li.active>a{font-weight:600}.skin-black-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-black-light .sidebar-form input[type="text"],.skin-black-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-black-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-black-light .sidebar-form input[type="text"]:focus,.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-black-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-black-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-green .main-header .navbar{background-color:#00a65a}.skin-green .main-header .navbar .nav>li>a{color:#fff}.skin-green .main-header .navbar .nav>li>a:hover,.skin-green .main-header .navbar .nav>li>a:active,.skin-green .main-header .navbar .nav>li>a:focus,.skin-green .main-header .navbar .nav .open>a,.skin-green .main-header .navbar .nav .open>a:hover,.skin-green .main-header .navbar .nav .open>a:focus,.skin-green .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-green .main-header .navbar .sidebar-toggle{color:#fff}.skin-green .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-green .main-header .navbar .sidebar-toggle{color:#fff}.skin-green .main-header .navbar .sidebar-toggle:hover{background-color:#008d4c}@media (max-width:767px){.skin-green .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-green .main-header .navbar .dropdown-menu li a{color:#fff}.skin-green .main-header .navbar .dropdown-menu li a:hover{background:#008d4c}}.skin-green .main-header .logo{background-color:#008d4c;color:#fff;border-bottom:0 solid transparent}.skin-green .main-header .logo:hover{background-color:#008749}.skin-green .main-header li.user-header{background-color:#00a65a}.skin-green .content-header{background:transparent}.skin-green .wrapper,.skin-green .main-sidebar,.skin-green .left-side{background-color:#222d32}.skin-green .user-panel>.info,.skin-green .user-panel>.info>a{color:#fff}.skin-green .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-green .sidebar-menu>li>a{border-left:3px solid transparent}.skin-green .sidebar-menu>li:hover>a,.skin-green .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#00a65a}.skin-green .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-green .sidebar a{color:#b8c7ce}.skin-green .sidebar a:hover{text-decoration:none}.skin-green .treeview-menu>li>a{color:#8aa4af}.skin-green .treeview-menu>li.active>a,.skin-green .treeview-menu>li>a:hover{color:#fff}.skin-green .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-green .sidebar-form input[type="text"],.skin-green .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-green .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-green .sidebar-form input[type="text"]:focus,.skin-green .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-green .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-green .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-green-light .main-header .navbar{background-color:#00a65a}.skin-green-light .main-header .navbar .nav>li>a{color:#fff}.skin-green-light .main-header .navbar .nav>li>a:hover,.skin-green-light .main-header .navbar .nav>li>a:active,.skin-green-light .main-header .navbar .nav>li>a:focus,.skin-green-light .main-header .navbar .nav .open>a,.skin-green-light .main-header .navbar .nav .open>a:hover,.skin-green-light .main-header .navbar .nav .open>a:focus,.skin-green-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-green-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-green-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-green-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-green-light .main-header .navbar .sidebar-toggle:hover{background-color:#008d4c}@media (max-width:767px){.skin-green-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-green-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-green-light .main-header .navbar .dropdown-menu li a:hover{background:#008d4c}}.skin-green-light .main-header .logo{background-color:#00a65a;color:#fff;border-bottom:0 solid transparent}.skin-green-light .main-header .logo:hover{background-color:#00a157}.skin-green-light .main-header li.user-header{background-color:#00a65a}.skin-green-light .content-header{background:transparent}.skin-green-light .wrapper,.skin-green-light .main-sidebar,.skin-green-light .left-side{background-color:#f9fafc}.skin-green-light .content-wrapper,.skin-green-light .main-footer{border-left:1px solid #d2d6de}.skin-green-light .user-panel>.info,.skin-green-light .user-panel>.info>a{color:#444}.skin-green-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-green-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-green-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-green-light .sidebar-menu>li:hover>a,.skin-green-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-green-light .sidebar-menu>li.active{border-left-color:#00a65a}.skin-green-light .sidebar-menu>li.active>a{font-weight:600}.skin-green-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-green-light .sidebar a{color:#444}.skin-green-light .sidebar a:hover{text-decoration:none}.skin-green-light .treeview-menu>li>a{color:#777}.skin-green-light .treeview-menu>li.active>a,.skin-green-light .treeview-menu>li>a:hover{color:#000}.skin-green-light .treeview-menu>li.active>a{font-weight:600}.skin-green-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-green-light .sidebar-form input[type="text"],.skin-green-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-green-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-green-light .sidebar-form input[type="text"]:focus,.skin-green-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-green-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-green-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-green-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-red .main-header .navbar{background-color:#dd4b39}.skin-red .main-header .navbar .nav>li>a{color:#fff}.skin-red .main-header .navbar .nav>li>a:hover,.skin-red .main-header .navbar .nav>li>a:active,.skin-red .main-header .navbar .nav>li>a:focus,.skin-red .main-header .navbar .nav .open>a,.skin-red .main-header .navbar .nav .open>a:hover,.skin-red .main-header .navbar .nav .open>a:focus,.skin-red .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-red .main-header .navbar .sidebar-toggle{color:#fff}.skin-red .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-red .main-header .navbar .sidebar-toggle{color:#fff}.skin-red .main-header .navbar .sidebar-toggle:hover{background-color:#d73925}@media (max-width:767px){.skin-red .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-red .main-header .navbar .dropdown-menu li a{color:#fff}.skin-red .main-header .navbar .dropdown-menu li a:hover{background:#d73925}}.skin-red .main-header .logo{background-color:#d73925;color:#fff;border-bottom:0 solid transparent}.skin-red .main-header .logo:hover{background-color:#d33724}.skin-red .main-header li.user-header{background-color:#dd4b39}.skin-red .content-header{background:transparent}.skin-red .wrapper,.skin-red .main-sidebar,.skin-red .left-side{background-color:#222d32}.skin-red .user-panel>.info,.skin-red .user-panel>.info>a{color:#fff}.skin-red .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-red .sidebar-menu>li>a{border-left:3px solid transparent}.skin-red .sidebar-menu>li:hover>a,.skin-red .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#dd4b39}.skin-red .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-red .sidebar a{color:#b8c7ce}.skin-red .sidebar a:hover{text-decoration:none}.skin-red .treeview-menu>li>a{color:#8aa4af}.skin-red .treeview-menu>li.active>a,.skin-red .treeview-menu>li>a:hover{color:#fff}.skin-red .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-red .sidebar-form input[type="text"],.skin-red .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-red .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-red .sidebar-form input[type="text"]:focus,.skin-red .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-red .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-red .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-red-light .main-header .navbar{background-color:#dd4b39}.skin-red-light .main-header .navbar .nav>li>a{color:#fff}.skin-red-light .main-header .navbar .nav>li>a:hover,.skin-red-light .main-header .navbar .nav>li>a:active,.skin-red-light .main-header .navbar .nav>li>a:focus,.skin-red-light .main-header .navbar .nav .open>a,.skin-red-light .main-header .navbar .nav .open>a:hover,.skin-red-light .main-header .navbar .nav .open>a:focus,.skin-red-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-red-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-red-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-red-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-red-light .main-header .navbar .sidebar-toggle:hover{background-color:#d73925}@media (max-width:767px){.skin-red-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-red-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-red-light .main-header .navbar .dropdown-menu li a:hover{background:#d73925}}.skin-red-light .main-header .logo{background-color:#dd4b39;color:#fff;border-bottom:0 solid transparent}.skin-red-light .main-header .logo:hover{background-color:#dc4735}.skin-red-light .main-header li.user-header{background-color:#dd4b39}.skin-red-light .content-header{background:transparent}.skin-red-light .wrapper,.skin-red-light .main-sidebar,.skin-red-light .left-side{background-color:#f9fafc}.skin-red-light .content-wrapper,.skin-red-light .main-footer{border-left:1px solid #d2d6de}.skin-red-light .user-panel>.info,.skin-red-light .user-panel>.info>a{color:#444}.skin-red-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-red-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-red-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-red-light .sidebar-menu>li:hover>a,.skin-red-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-red-light .sidebar-menu>li.active{border-left-color:#dd4b39}.skin-red-light .sidebar-menu>li.active>a{font-weight:600}.skin-red-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-red-light .sidebar a{color:#444}.skin-red-light .sidebar a:hover{text-decoration:none}.skin-red-light .treeview-menu>li>a{color:#777}.skin-red-light .treeview-menu>li.active>a,.skin-red-light .treeview-menu>li>a:hover{color:#000}.skin-red-light .treeview-menu>li.active>a{font-weight:600}.skin-red-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-red-light .sidebar-form input[type="text"],.skin-red-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-red-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-red-light .sidebar-form input[type="text"]:focus,.skin-red-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-red-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-red-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-red-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-yellow .main-header .navbar{background-color:#f39c12}.skin-yellow .main-header .navbar .nav>li>a{color:#fff}.skin-yellow .main-header .navbar .nav>li>a:hover,.skin-yellow .main-header .navbar .nav>li>a:active,.skin-yellow .main-header .navbar .nav>li>a:focus,.skin-yellow .main-header .navbar .nav .open>a,.skin-yellow .main-header .navbar .nav .open>a:hover,.skin-yellow .main-header .navbar .nav .open>a:focus,.skin-yellow .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-yellow .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-yellow .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow .main-header .navbar .sidebar-toggle:hover{background-color:#e08e0b}@media (max-width:767px){.skin-yellow .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-yellow .main-header .navbar .dropdown-menu li a{color:#fff}.skin-yellow .main-header .navbar .dropdown-menu li a:hover{background:#e08e0b}}.skin-yellow .main-header .logo{background-color:#e08e0b;color:#fff;border-bottom:0 solid transparent}.skin-yellow .main-header .logo:hover{background-color:#db8b0b}.skin-yellow .main-header li.user-header{background-color:#f39c12}.skin-yellow .content-header{background:transparent}.skin-yellow .wrapper,.skin-yellow .main-sidebar,.skin-yellow .left-side{background-color:#222d32}.skin-yellow .user-panel>.info,.skin-yellow .user-panel>.info>a{color:#fff}.skin-yellow .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-yellow .sidebar-menu>li>a{border-left:3px solid transparent}.skin-yellow .sidebar-menu>li:hover>a,.skin-yellow .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#f39c12}.skin-yellow .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-yellow .sidebar a{color:#b8c7ce}.skin-yellow .sidebar a:hover{text-decoration:none}.skin-yellow .treeview-menu>li>a{color:#8aa4af}.skin-yellow .treeview-menu>li.active>a,.skin-yellow .treeview-menu>li>a:hover{color:#fff}.skin-yellow .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-yellow .sidebar-form input[type="text"],.skin-yellow .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-yellow .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-yellow .sidebar-form input[type="text"]:focus,.skin-yellow .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-yellow .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-yellow .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-yellow-light .main-header .navbar{background-color:#f39c12}.skin-yellow-light .main-header .navbar .nav>li>a{color:#fff}.skin-yellow-light .main-header .navbar .nav>li>a:hover,.skin-yellow-light .main-header .navbar .nav>li>a:active,.skin-yellow-light .main-header .navbar .nav>li>a:focus,.skin-yellow-light .main-header .navbar .nav .open>a,.skin-yellow-light .main-header .navbar .nav .open>a:hover,.skin-yellow-light .main-header .navbar .nav .open>a:focus,.skin-yellow-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-yellow-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-yellow-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-yellow-light .main-header .navbar .sidebar-toggle:hover{background-color:#e08e0b}@media (max-width:767px){.skin-yellow-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-yellow-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-yellow-light .main-header .navbar .dropdown-menu li a:hover{background:#e08e0b}}.skin-yellow-light .main-header .logo{background-color:#f39c12;color:#fff;border-bottom:0 solid transparent}.skin-yellow-light .main-header .logo:hover{background-color:#f39a0d}.skin-yellow-light .main-header li.user-header{background-color:#f39c12}.skin-yellow-light .content-header{background:transparent}.skin-yellow-light .wrapper,.skin-yellow-light .main-sidebar,.skin-yellow-light .left-side{background-color:#f9fafc}.skin-yellow-light .content-wrapper,.skin-yellow-light .main-footer{border-left:1px solid #d2d6de}.skin-yellow-light .user-panel>.info,.skin-yellow-light .user-panel>.info>a{color:#444}.skin-yellow-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-yellow-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-yellow-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-yellow-light .sidebar-menu>li:hover>a,.skin-yellow-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-yellow-light .sidebar-menu>li.active{border-left-color:#f39c12}.skin-yellow-light .sidebar-menu>li.active>a{font-weight:600}.skin-yellow-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-yellow-light .sidebar a{color:#444}.skin-yellow-light .sidebar a:hover{text-decoration:none}.skin-yellow-light .treeview-menu>li>a{color:#777}.skin-yellow-light .treeview-menu>li.active>a,.skin-yellow-light .treeview-menu>li>a:hover{color:#000}.skin-yellow-light .treeview-menu>li.active>a{font-weight:600}.skin-yellow-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-yellow-light .sidebar-form input[type="text"],.skin-yellow-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-yellow-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-yellow-light .sidebar-form input[type="text"]:focus,.skin-yellow-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-yellow-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-yellow-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-yellow-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}}.skin-purple .main-header .navbar{background-color:#605ca8}.skin-purple .main-header .navbar .nav>li>a{color:#fff}.skin-purple .main-header .navbar .nav>li>a:hover,.skin-purple .main-header .navbar .nav>li>a:active,.skin-purple .main-header .navbar .nav>li>a:focus,.skin-purple .main-header .navbar .nav .open>a,.skin-purple .main-header .navbar .nav .open>a:hover,.skin-purple .main-header .navbar .nav .open>a:focus,.skin-purple .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-purple .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-purple .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple .main-header .navbar .sidebar-toggle:hover{background-color:#555299}@media (max-width:767px){.skin-purple .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-purple .main-header .navbar .dropdown-menu li a{color:#fff}.skin-purple .main-header .navbar .dropdown-menu li a:hover{background:#555299}}.skin-purple .main-header .logo{background-color:#555299;color:#fff;border-bottom:0 solid transparent}.skin-purple .main-header .logo:hover{background-color:#545096}.skin-purple .main-header li.user-header{background-color:#605ca8}.skin-purple .content-header{background:transparent}.skin-purple .wrapper,.skin-purple .main-sidebar,.skin-purple .left-side{background-color:#222d32}.skin-purple .user-panel>.info,.skin-purple .user-panel>.info>a{color:#fff}.skin-purple .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-purple .sidebar-menu>li>a{border-left:3px solid transparent}.skin-purple .sidebar-menu>li:hover>a,.skin-purple .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#605ca8}.skin-purple .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-purple .sidebar a{color:#b8c7ce}.skin-purple .sidebar a:hover{text-decoration:none}.skin-purple .treeview-menu>li>a{color:#8aa4af}.skin-purple .treeview-menu>li.active>a,.skin-purple .treeview-menu>li>a:hover{color:#fff}.skin-purple .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-purple .sidebar-form input[type="text"],.skin-purple .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-purple .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-purple .sidebar-form input[type="text"]:focus,.skin-purple .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-purple .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-purple .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-purple-light .main-header .navbar{background-color:#605ca8}.skin-purple-light .main-header .navbar .nav>li>a{color:#fff}.skin-purple-light .main-header .navbar .nav>li>a:hover,.skin-purple-light .main-header .navbar .nav>li>a:active,.skin-purple-light .main-header .navbar .nav>li>a:focus,.skin-purple-light .main-header .navbar .nav .open>a,.skin-purple-light .main-header .navbar .nav .open>a:hover,.skin-purple-light .main-header .navbar .nav .open>a:focus,.skin-purple-light .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-purple-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple-light .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-purple-light .main-header .navbar .sidebar-toggle{color:#fff}.skin-purple-light .main-header .navbar .sidebar-toggle:hover{background-color:#555299}@media (max-width:767px){.skin-purple-light .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-purple-light .main-header .navbar .dropdown-menu li a{color:#fff}.skin-purple-light .main-header .navbar .dropdown-menu li a:hover{background:#555299}}.skin-purple-light .main-header .logo{background-color:#605ca8;color:#fff;border-bottom:0 solid transparent}.skin-purple-light .main-header .logo:hover{background-color:#5d59a6}.skin-purple-light .main-header li.user-header{background-color:#605ca8}.skin-purple-light .content-header{background:transparent}.skin-purple-light .wrapper,.skin-purple-light .main-sidebar,.skin-purple-light .left-side{background-color:#f9fafc}.skin-purple-light .content-wrapper,.skin-purple-light .main-footer{border-left:1px solid #d2d6de}.skin-purple-light .user-panel>.info,.skin-purple-light .user-panel>.info>a{color:#444}.skin-purple-light .sidebar-menu>li{-webkit-transition:border-left-color .3s ease;-o-transition:border-left-color .3s ease;transition:border-left-color .3s ease}.skin-purple-light .sidebar-menu>li.header{color:#848484;background:#f9fafc}.skin-purple-light .sidebar-menu>li>a{border-left:3px solid transparent;font-weight:600}.skin-purple-light .sidebar-menu>li:hover>a,.skin-purple-light .sidebar-menu>li.active>a{color:#000;background:#f4f4f5}.skin-purple-light .sidebar-menu>li.active{border-left-color:#605ca8}.skin-purple-light .sidebar-menu>li.active>a{font-weight:600}.skin-purple-light .sidebar-menu>li>.treeview-menu{background:#f4f4f5}.skin-purple-light .sidebar a{color:#444}.skin-purple-light .sidebar a:hover{text-decoration:none}.skin-purple-light .treeview-menu>li>a{color:#777}.skin-purple-light .treeview-menu>li.active>a,.skin-purple-light .treeview-menu>li>a:hover{color:#000}.skin-purple-light .treeview-menu>li.active>a{font-weight:600}.skin-purple-light .sidebar-form{border-radius:3px;border:1px solid #d2d6de;margin:10px 10px}.skin-purple-light .sidebar-form input[type="text"],.skin-purple-light .sidebar-form .btn{box-shadow:none;background-color:#fff;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-purple-light .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-purple-light .sidebar-form input[type="text"]:focus,.skin-purple-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-purple-light .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-purple-light .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}@media (min-width:768px){.skin-purple-light.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{border-left:1px solid #d2d6de}} \ No newline at end of file diff --git a/web/static/dist/css/skins/skin-blue.css b/web/static/dist/css/skins/skin-blue.css new file mode 100644 index 0000000..1bc543f --- /dev/null +++ b/web/static/dist/css/skins/skin-blue.css @@ -0,0 +1,142 @@ +/* + * Skin: Blue + * ---------- + */ +.skin-blue .main-header .navbar { + background-color: #3c8dbc; +} +.skin-blue .main-header .navbar .nav > li > a { + color: #ffffff; +} +.skin-blue .main-header .navbar .nav > li > a:hover, +.skin-blue .main-header .navbar .nav > li > a:active, +.skin-blue .main-header .navbar .nav > li > a:focus, +.skin-blue .main-header .navbar .nav .open > a, +.skin-blue .main-header .navbar .nav .open > a:hover, +.skin-blue .main-header .navbar .nav .open > a:focus, +.skin-blue .main-header .navbar .nav > .active > a { + background: rgba(0, 0, 0, 0.1); + color: #f6f6f6; +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #ffffff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + color: #f6f6f6; + background: rgba(0, 0, 0, 0.1); +} +.skin-blue .main-header .navbar .sidebar-toggle { + color: #fff; +} +.skin-blue .main-header .navbar .sidebar-toggle:hover { + background-color: #367fa9; +} +@media (max-width: 767px) { + .skin-blue .main-header .navbar .dropdown-menu li.divider { + background-color: rgba(255, 255, 255, 0.1); + } + .skin-blue .main-header .navbar .dropdown-menu li a { + color: #fff; + } + .skin-blue .main-header .navbar .dropdown-menu li a:hover { + background: #367fa9; + } +} +.skin-blue .main-header .logo { + background-color: #367fa9; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue .main-header .logo:hover { + background-color: #357ca5; +} +.skin-blue .main-header li.user-header { + background-color: #3c8dbc; +} +.skin-blue .content-header { + background: transparent; +} +.skin-blue .wrapper, +.skin-blue .main-sidebar, +.skin-blue .left-side { + background-color: #222d32; +} +.skin-blue .user-panel > .info, +.skin-blue .user-panel > .info > a { + color: #fff; +} +.skin-blue .sidebar-menu > li.header { + color: #4b646f; + background: #1a2226; +} +.skin-blue .sidebar-menu > li > a { + border-left: 3px solid transparent; +} +.skin-blue .sidebar-menu > li:hover > a, +.skin-blue .sidebar-menu > li.active > a { + color: #ffffff; + background: #1e282c; + border-left-color: #3c8dbc; +} +.skin-blue .sidebar-menu > li > .treeview-menu { + margin: 0 1px; + background: #2c3b41; +} +.skin-blue .sidebar a { + color: #b8c7ce; +} +.skin-blue .sidebar a:hover { + text-decoration: none; +} +.skin-blue .treeview-menu > li > a { + color: #8aa4af; +} +.skin-blue .treeview-menu > li.active > a, +.skin-blue .treeview-menu > li > a:hover { + color: #ffffff; +} +.skin-blue .sidebar-form { + border-radius: 3px; + border: 1px solid #374850; + margin: 10px 10px; +} +.skin-blue .sidebar-form input[type="text"], +.skin-blue .sidebar-form .btn { + box-shadow: none; + background-color: #374850; + border: 1px solid transparent; + height: 35px; + -webkit-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; +} +.skin-blue .sidebar-form input[type="text"] { + color: #666; + border-top-left-radius: 2px; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 2px; +} +.skin-blue .sidebar-form input[type="text"]:focus, +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + background-color: #fff; + color: #666; +} +.skin-blue .sidebar-form input[type="text"]:focus + .input-group-btn .btn { + border-left-color: #fff; +} +.skin-blue .sidebar-form .btn { + color: #999; + border-top-left-radius: 0; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + border-bottom-left-radius: 0; +} +.skin-blue.layout-top-nav .main-header > .logo { + background-color: #3c8dbc; + color: #ffffff; + border-bottom: 0 solid transparent; +} +.skin-blue.layout-top-nav .main-header > .logo:hover { + background-color: #3b8ab8; +} diff --git a/web/static/dist/css/skins/skin-blue.min.css b/web/static/dist/css/skins/skin-blue.min.css new file mode 100644 index 0000000..123c04f --- /dev/null +++ b/web/static/dist/css/skins/skin-blue.min.css @@ -0,0 +1 @@ +.skin-blue .main-header .navbar{background-color:#3c8dbc}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-blue .main-header .logo{background-color:#367fa9;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#357ca5}.skin-blue .main-header li.user-header{background-color:#3c8dbc}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#222d32}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#3c8dbc}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-blue .sidebar a{color:#b8c7ce}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .treeview-menu>li>a{color:#8aa4af}.skin-blue .treeview-menu>li.active>a,.skin-blue .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px;-webkit-transition:all .3s ease-in-out;-o-transition:all .3s ease-in-out;transition:all .3s ease-in-out}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8} \ No newline at end of file diff --git a/web/static/dist/js/app.js b/web/static/dist/js/app.js new file mode 100644 index 0000000..b016ffe --- /dev/null +++ b/web/static/dist/js/app.js @@ -0,0 +1,758 @@ +/*! AdminLTE app.js + * ================ + * Main JS application file for AdminLTE v2. This file + * should be included in all pages. It controls some layout + * options and implements exclusive AdminLTE plugins. + * + * @Author Almsaeed Studio + * @Support + * @Email + * @version 2.3.2 + * @license MIT + */ + +//Make sure jQuery has been loaded before app.js +if (typeof jQuery === "undefined") { + throw new Error("AdminLTE requires jQuery"); +} + +/* AdminLTE + * + * @type Object + * @description $.AdminLTE is the main object for the template's app. + * It's used for implementing functions and options related + * to the template. Keeping everything wrapped in an object + * prevents conflict with other plugins and is a better + * way to organize our code. + */ +$.AdminLTE = {}; + +/* -------------------- + * - AdminLTE Options - + * -------------------- + * Modify these options to suit your implementation + */ +$.AdminLTE.options = { + //Add slimscroll to navbar menus + //This requires you to load the slimscroll plugin + //in every page before app.js + navbarMenuSlimscroll: true, + navbarMenuSlimscrollWidth: "3px", //The width of the scroll bar + navbarMenuHeight: "200px", //The height of the inner menu + //General animation speed for JS animated elements such as box collapse/expand and + //sidebar treeview slide up/down. This options accepts an integer as milliseconds, + //'fast', 'normal', or 'slow' + animationSpeed: 500, + //Sidebar push menu toggle button selector + sidebarToggleSelector: "[data-toggle='offcanvas']", + //Activate sidebar push menu + sidebarPushMenu: true, + //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin) + sidebarSlimScroll: true, + //Enable sidebar expand on hover effect for sidebar mini + //This option is forced to true if both the fixed layout and sidebar mini + //are used together + sidebarExpandOnHover: false, + //BoxRefresh Plugin + enableBoxRefresh: true, + //Bootstrap.js tooltip + enableBSToppltip: true, + BSTooltipSelector: "[data-toggle='tooltip']", + //Enable Fast Click. Fastclick.js creates a more + //native touch experience with touch devices. If you + //choose to enable the plugin, make sure you load the script + //before AdminLTE's app.js + enableFastclick: true, + //Control Sidebar Options + enableControlSidebar: true, + controlSidebarOptions: { + //Which button should trigger the open/close event + toggleBtnSelector: "[data-toggle='control-sidebar']", + //The sidebar selector + selector: ".control-sidebar", + //Enable slide over content + slide: true + }, + //Box Widget Plugin. Enable this plugin + //to allow boxes to be collapsed and/or removed + enableBoxWidget: true, + //Box Widget plugin options + boxWidgetOptions: { + boxWidgetIcons: { + //Collapse icon + collapse: 'fa-minus', + //Open icon + open: 'fa-plus', + //Remove icon + remove: 'fa-times' + }, + boxWidgetSelectors: { + //Remove button selector + remove: '[data-widget="remove"]', + //Collapse button selector + collapse: '[data-widget="collapse"]' + } + }, + //Direct Chat plugin options + directChat: { + //Enable direct chat by default + enable: true, + //The button to open and close the chat contacts pane + contactToggleSelector: '[data-widget="chat-pane-toggle"]' + }, + //Define the set of colors to use globally around the website + colors: { + lightBlue: "#3c8dbc", + red: "#f56954", + green: "#00a65a", + aqua: "#00c0ef", + yellow: "#f39c12", + blue: "#0073b7", + navy: "#001F3F", + teal: "#39CCCC", + olive: "#3D9970", + lime: "#01FF70", + orange: "#FF851B", + fuchsia: "#F012BE", + purple: "#8E24AA", + maroon: "#D81B60", + black: "#222222", + gray: "#d2d6de" + }, + //The standard screen sizes that bootstrap uses. + //If you change these in the variables.less file, change + //them here too. + screenSizes: { + xs: 480, + sm: 768, + md: 992, + lg: 1200 + } +}; + +/* ------------------ + * - Implementation - + * ------------------ + * The next block of code implements AdminLTE's + * functions and plugins as specified by the + * options above. + */ +$(function () { + "use strict"; + + //Fix for IE page transitions + $("body").removeClass("hold-transition"); + + //Extend options if external options exist + if (typeof AdminLTEOptions !== "undefined") { + $.extend(true, + $.AdminLTE.options, + AdminLTEOptions); + } + + //Easy access to options + var o = $.AdminLTE.options; + + //Set up the object + _init(); + + //Activate the layout maker + $.AdminLTE.layout.activate(); + + //Enable sidebar tree view controls + $.AdminLTE.tree('.sidebar'); + + //Enable control sidebar + if (o.enableControlSidebar) { + $.AdminLTE.controlSidebar.activate(); + } + + //Add slimscroll to navbar dropdown + if (o.navbarMenuSlimscroll && typeof $.fn.slimscroll != 'undefined') { + $(".navbar .menu").slimscroll({ + height: o.navbarMenuHeight, + alwaysVisible: false, + size: o.navbarMenuSlimscrollWidth + }).css("width", "100%"); + } + + //Activate sidebar push menu + if (o.sidebarPushMenu) { + $.AdminLTE.pushMenu.activate(o.sidebarToggleSelector); + } + + //Activate Bootstrap tooltip + if (o.enableBSToppltip) { + $('body').tooltip({ + selector: o.BSTooltipSelector + }); + } + + //Activate box widget + if (o.enableBoxWidget) { + $.AdminLTE.boxWidget.activate(); + } + + //Activate fast click + if (o.enableFastclick && typeof FastClick != 'undefined') { + FastClick.attach(document.body); + } + + //Activate direct chat widget + if (o.directChat.enable) { + $(document).on('click', o.directChat.contactToggleSelector, function () { + var box = $(this).parents('.direct-chat').first(); + box.toggleClass('direct-chat-contacts-open'); + }); + } + + /* + * INITIALIZE BUTTON TOGGLE + * ------------------------ + */ + $('.btn-group[data-toggle="btn-toggle"]').each(function () { + var group = $(this); + $(this).find(".btn").on('click', function (e) { + group.find(".btn.active").removeClass("active"); + $(this).addClass("active"); + e.preventDefault(); + }); + + }); +}); + +/* ---------------------------------- + * - Initialize the AdminLTE Object - + * ---------------------------------- + * All AdminLTE functions are implemented below. + */ +function _init() { + 'use strict'; + /* Layout + * ====== + * Fixes the layout height in case min-height fails. + * + * @type Object + * @usage $.AdminLTE.layout.activate() + * $.AdminLTE.layout.fix() + * $.AdminLTE.layout.fixSidebar() + */ + $.AdminLTE.layout = { + activate: function () { + var _this = this; + _this.fix(); + _this.fixSidebar(); + $(window, ".wrapper").resize(function () { + _this.fix(); + _this.fixSidebar(); + }); + }, + fix: function () { + //Get window height and the wrapper height + var neg = $('.main-header').outerHeight() + $('.main-footer').outerHeight(); + var window_height = $(window).height(); + var sidebar_height = $(".sidebar").height(); + //Set the min-height of the content and sidebar based on the + //the height of the document. + if ($("body").hasClass("fixed")) { + $(".content-wrapper, .right-side").css('min-height', window_height - $('.main-footer').outerHeight()); + } else { + var postSetWidth; + if (window_height >= sidebar_height) { + $(".content-wrapper, .right-side").css('min-height', window_height - neg); + postSetWidth = window_height - neg; + } else { + $(".content-wrapper, .right-side").css('min-height', sidebar_height); + postSetWidth = sidebar_height; + } + + //Fix for the control sidebar height + var controlSidebar = $($.AdminLTE.options.controlSidebarOptions.selector); + if (typeof controlSidebar !== "undefined") { + if (controlSidebar.height() > postSetWidth) + $(".content-wrapper, .right-side").css('min-height', controlSidebar.height()); + } + + } + }, + fixSidebar: function () { + //Make sure the body tag has the .fixed class + if (!$("body").hasClass("fixed")) { + if (typeof $.fn.slimScroll != 'undefined') { + $(".sidebar").slimScroll({destroy: true}).height("auto"); + } + return; + } else if (typeof $.fn.slimScroll == 'undefined' && window.console) { + window.console.error("Error: the fixed layout requires the slimscroll plugin!"); + } + //Enable slimscroll for fixed layout + if ($.AdminLTE.options.sidebarSlimScroll) { + if (typeof $.fn.slimScroll != 'undefined') { + //Destroy if it exists + $(".sidebar").slimScroll({destroy: true}).height("auto"); + //Add slimscroll + $(".sidebar").slimscroll({ + height: ($(window).height() - $(".main-header").height()) + "px", + color: "rgba(0,0,0,0.2)", + size: "3px" + }); + } + } + } + }; + + /* PushMenu() + * ========== + * Adds the push menu functionality to the sidebar. + * + * @type Function + * @usage: $.AdminLTE.pushMenu("[data-toggle='offcanvas']") + */ + $.AdminLTE.pushMenu = { + activate: function (toggleBtn) { + //Get the screen sizes + var screenSizes = $.AdminLTE.options.screenSizes; + + //Enable sidebar toggle + $(document).on('click', toggleBtn, function (e) { + e.preventDefault(); + + //Enable sidebar push menu + if ($(window).width() > (screenSizes.sm - 1)) { + if ($("body").hasClass('sidebar-collapse')) { + $("body").removeClass('sidebar-collapse').trigger('expanded.pushMenu'); + } else { + $("body").addClass('sidebar-collapse').trigger('collapsed.pushMenu'); + } + } + //Handle sidebar push menu for small screens + else { + if ($("body").hasClass('sidebar-open')) { + $("body").removeClass('sidebar-open').removeClass('sidebar-collapse').trigger('collapsed.pushMenu'); + } else { + $("body").addClass('sidebar-open').trigger('expanded.pushMenu'); + } + } + }); + + $(".content-wrapper").click(function () { + //Enable hide menu when clicking on the content-wrapper on small screens + if ($(window).width() <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open")) { + $("body").removeClass('sidebar-open'); + } + }); + + //Enable expand on hover for sidebar mini + if ($.AdminLTE.options.sidebarExpandOnHover + || ($('body').hasClass('fixed') + && $('body').hasClass('sidebar-mini'))) { + this.expandOnHover(); + } + }, + expandOnHover: function () { + var _this = this; + var screenWidth = $.AdminLTE.options.screenSizes.sm - 1; + //Expand sidebar on hover + $('.main-sidebar').hover(function () { + if ($('body').hasClass('sidebar-mini') + && $("body").hasClass('sidebar-collapse') + && $(window).width() > screenWidth) { + _this.expand(); + } + }, function () { + if ($('body').hasClass('sidebar-mini') + && $('body').hasClass('sidebar-expanded-on-hover') + && $(window).width() > screenWidth) { + _this.collapse(); + } + }); + }, + expand: function () { + $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover'); + }, + collapse: function () { + if ($('body').hasClass('sidebar-expanded-on-hover')) { + $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse'); + } + } + }; + + /* Tree() + * ====== + * Converts the sidebar into a multilevel + * tree view menu. + * + * @type Function + * @Usage: $.AdminLTE.tree('.sidebar') + */ + $.AdminLTE.tree = function (menu) { + var _this = this; + var animationSpeed = $.AdminLTE.options.animationSpeed; + $(document).on('click', menu + ' li a', function (e) { + //Get the clicked link and the next element + var $this = $(this); + var checkElement = $this.next(); + + //Check if the next element is a menu and is visible + if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) { + //Close the menu + checkElement.slideUp(animationSpeed, function () { + checkElement.removeClass('menu-open'); + //Fix the layout in case the sidebar stretches over the height of the window + //_this.layout.fix(); + }); + checkElement.parent("li").removeClass("active"); + } + //If the menu is not visible + else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) { + //Get the parent menu + var parent = $this.parents('ul').first(); + //Close all open menus within the parent + var ul = parent.find('ul:visible').slideUp(animationSpeed); + //Remove the menu-open class from the parent + ul.removeClass('menu-open'); + //Get the parent li + var parent_li = $this.parent("li"); + + //Open the target menu and add the menu-open class + checkElement.slideDown(animationSpeed, function () { + //Add the class active to the parent li + checkElement.addClass('menu-open'); + parent.find('li.active').removeClass('active'); + parent_li.addClass('active'); + //Fix the layout in case the sidebar stretches over the height of the window + _this.layout.fix(); + }); + } + //if this isn't a link, prevent the page from being redirected + if (checkElement.is('.treeview-menu')) { + e.preventDefault(); + } + }); + }; + + /* ControlSidebar + * ============== + * Adds functionality to the right sidebar + * + * @type Object + * @usage $.AdminLTE.controlSidebar.activate(options) + */ + $.AdminLTE.controlSidebar = { + //instantiate the object + activate: function () { + //Get the object + var _this = this; + //Update options + var o = $.AdminLTE.options.controlSidebarOptions; + //Get the sidebar + var sidebar = $(o.selector); + //The toggle button + var btn = $(o.toggleBtnSelector); + + //Listen to the click event + btn.on('click', function (e) { + e.preventDefault(); + //If the sidebar is not open + if (!sidebar.hasClass('control-sidebar-open') + && !$('body').hasClass('control-sidebar-open')) { + //Open the sidebar + _this.open(sidebar, o.slide); + } else { + _this.close(sidebar, o.slide); + } + }); + + //If the body has a boxed layout, fix the sidebar bg position + var bg = $(".control-sidebar-bg"); + _this._fix(bg); + + //If the body has a fixed layout, make the control sidebar fixed + if ($('body').hasClass('fixed')) { + _this._fixForFixed(sidebar); + } else { + //If the content height is less than the sidebar's height, force max height + if ($('.content-wrapper, .right-side').height() < sidebar.height()) { + _this._fixForContent(sidebar); + } + } + }, + //Open the control sidebar + open: function (sidebar, slide) { + //Slide over content + if (slide) { + sidebar.addClass('control-sidebar-open'); + } else { + //Push the content by adding the open class to the body instead + //of the sidebar itself + $('body').addClass('control-sidebar-open'); + } + }, + //Close the control sidebar + close: function (sidebar, slide) { + if (slide) { + sidebar.removeClass('control-sidebar-open'); + } else { + $('body').removeClass('control-sidebar-open'); + } + }, + _fix: function (sidebar) { + var _this = this; + if ($("body").hasClass('layout-boxed')) { + sidebar.css('position', 'absolute'); + sidebar.height($(".wrapper").height()); + $(window).resize(function () { + _this._fix(sidebar); + }); + } else { + sidebar.css({ + 'position': 'fixed', + 'height': 'auto' + }); + } + }, + _fixForFixed: function (sidebar) { + sidebar.css({ + 'position': 'fixed', + 'max-height': '100%', + 'overflow': 'auto', + 'padding-bottom': '50px' + }); + }, + _fixForContent: function (sidebar) { + $(".content-wrapper, .right-side").css('min-height', sidebar.height()); + } + }; + + /* BoxWidget + * ========= + * BoxWidget is a plugin to handle collapsing and + * removing boxes from the screen. + * + * @type Object + * @usage $.AdminLTE.boxWidget.activate() + * Set all your options in the main $.AdminLTE.options object + */ + $.AdminLTE.boxWidget = { + selectors: $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors, + icons: $.AdminLTE.options.boxWidgetOptions.boxWidgetIcons, + animationSpeed: $.AdminLTE.options.animationSpeed, + activate: function (_box) { + var _this = this; + if (!_box) { + _box = document; // activate all boxes per default + } + //Listen for collapse event triggers + $(_box).on('click', _this.selectors.collapse, function (e) { + e.preventDefault(); + _this.collapse($(this)); + }); + + //Listen for remove event triggers + $(_box).on('click', _this.selectors.remove, function (e) { + e.preventDefault(); + _this.remove($(this)); + }); + }, + collapse: function (element) { + var _this = this; + //Find the box parent + var box = element.parents(".box").first(); + //Find the body and the footer + var box_content = box.find("> .box-body, > .box-footer, > form >.box-body, > form > .box-footer"); + if (!box.hasClass("collapsed-box")) { + //Convert minus into plus + element.children(":first") + .removeClass(_this.icons.collapse) + .addClass(_this.icons.open); + //Hide the content + box_content.slideUp(_this.animationSpeed, function () { + box.addClass("collapsed-box"); + }); + } else { + //Convert plus into minus + element.children(":first") + .removeClass(_this.icons.open) + .addClass(_this.icons.collapse); + //Show the content + box_content.slideDown(_this.animationSpeed, function () { + box.removeClass("collapsed-box"); + }); + } + }, + remove: function (element) { + //Find the box parent + var box = element.parents(".box").first(); + box.slideUp(this.animationSpeed); + } + }; +} + +/* ------------------ + * - Custom Plugins - + * ------------------ + * All custom plugins are defined below. + */ + +/* + * BOX REFRESH BUTTON + * ------------------ + * This is a custom plugin to use with the component BOX. It allows you to add + * a refresh button to the box. It converts the box's state to a loading state. + * + * @type plugin + * @usage $("#box-widget").boxRefresh( options ); + */ +(function ($) { + + "use strict"; + + $.fn.boxRefresh = function (options) { + + // Render options + var settings = $.extend({ + //Refresh button selector + trigger: ".refresh-btn", + //File source to be loaded (e.g: ajax/src.php) + source: "", + //Callbacks + onLoadStart: function (box) { + return box; + }, //Right after the button has been clicked + onLoadDone: function (box) { + return box; + } //When the source has been loaded + + }, options); + + //The overlay + var overlay = $('
'); + + return this.each(function () { + //if a source is specified + if (settings.source === "") { + if (window.console) { + window.console.log("Please specify a source first - boxRefresh()"); + } + return; + } + //the box + var box = $(this); + //the button + var rBtn = box.find(settings.trigger).first(); + + //On trigger click + rBtn.on('click', function (e) { + e.preventDefault(); + //Add loading overlay + start(box); + + //Perform ajax call + box.find(".box-body").load(settings.source, function () { + done(box); + }); + }); + }); + + function start(box) { + //Add overlay and loading img + box.append(overlay); + + settings.onLoadStart.call(box); + } + + function done(box) { + //Remove overlay and loading img + box.find(overlay).remove(); + + settings.onLoadDone.call(box); + } + + }; + +})(jQuery); + + /* + * EXPLICIT BOX CONTROLS + * ----------------------- + * This is a custom plugin to use with the component BOX. It allows you to activate + * a box inserted in the DOM after the app.js was loaded, toggle and remove box. + * + * @type plugin + * @usage $("#box-widget").activateBox(); + * @usage $("#box-widget").toggleBox(); + * @usage $("#box-widget").removeBox(); + */ +(function ($) { + + 'use strict'; + + $.fn.activateBox = function () { + $.AdminLTE.boxWidget.activate(this); + }; + + $.fn.toggleBox = function(){ + var button = $($.AdminLTE.boxWidget.selectors.collapse, this); + $.AdminLTE.boxWidget.collapse(button); + }; + + $.fn.removeBox = function(){ + var button = $($.AdminLTE.boxWidget.selectors.remove, this); + $.AdminLTE.boxWidget.remove(button); + }; + +})(jQuery); + +/* + * TODO LIST CUSTOM PLUGIN + * ----------------------- + * This plugin depends on iCheck plugin for checkbox and radio inputs + * + * @type plugin + * @usage $("#todo-widget").todolist( options ); + */ +(function ($) { + + 'use strict'; + + $.fn.todolist = function (options) { + // Render options + var settings = $.extend({ + //When the user checks the input + onCheck: function (ele) { + return ele; + }, + //When the user unchecks the input + onUncheck: function (ele) { + return ele; + } + }, options); + + return this.each(function () { + + if (typeof $.fn.iCheck != 'undefined') { + $('input', this).on('ifChecked', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + settings.onCheck.call(ele); + }); + + $('input', this).on('ifUnchecked', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + settings.onUncheck.call(ele); + }); + } else { + $('input', this).on('change', function () { + var ele = $(this).parents("li").first(); + ele.toggleClass("done"); + if ($('input', ele).is(":checked")) { + settings.onCheck.call(ele); + } else { + settings.onUncheck.call(ele); + } + }); + } + }); + }; +}(jQuery)); diff --git a/web/static/dist/js/app.min.js b/web/static/dist/js/app.min.js new file mode 100644 index 0000000..8ec5fe1 --- /dev/null +++ b/web/static/dist/js/app.min.js @@ -0,0 +1,13 @@ +/*! AdminLTE app.js + * ================ + * Main JS application file for AdminLTE v2. This file + * should be included in all pages. It controls some layout + * options and implements exclusive AdminLTE plugins. + * + * @Author Almsaeed Studio + * @Support + * @Email + * @version 2.3.2 + * @license MIT + */ +function _init(){"use strict";$.AdminLTE.layout={activate:function(){var a=this;a.fix(),a.fixSidebar(),$(window,".wrapper").resize(function(){a.fix(),a.fixSidebar()})},fix:function(){var a=$(".main-header").outerHeight()+$(".main-footer").outerHeight(),b=$(window).height(),c=$(".sidebar").height();if($("body").hasClass("fixed"))$(".content-wrapper, .right-side").css("min-height",b-$(".main-footer").outerHeight());else{var d;b>=c?($(".content-wrapper, .right-side").css("min-height",b-a),d=b-a):($(".content-wrapper, .right-side").css("min-height",c),d=c);var e=$($.AdminLTE.options.controlSidebarOptions.selector);"undefined"!=typeof e&&e.height()>d&&$(".content-wrapper, .right-side").css("min-height",e.height())}},fixSidebar:function(){return $("body").hasClass("fixed")?("undefined"==typeof $.fn.slimScroll&&window.console&&window.console.error("Error: the fixed layout requires the slimscroll plugin!"),void($.AdminLTE.options.sidebarSlimScroll&&"undefined"!=typeof $.fn.slimScroll&&($(".sidebar").slimScroll({destroy:!0}).height("auto"),$(".sidebar").slimscroll({height:$(window).height()-$(".main-header").height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"})))):void("undefined"!=typeof $.fn.slimScroll&&$(".sidebar").slimScroll({destroy:!0}).height("auto"))}},$.AdminLTE.pushMenu={activate:function(a){var b=$.AdminLTE.options.screenSizes;$(document).on("click",a,function(a){a.preventDefault(),$(window).width()>b.sm-1?$("body").hasClass("sidebar-collapse")?$("body").removeClass("sidebar-collapse").trigger("expanded.pushMenu"):$("body").addClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").hasClass("sidebar-open")?$("body").removeClass("sidebar-open").removeClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").addClass("sidebar-open").trigger("expanded.pushMenu")}),$(".content-wrapper").click(function(){$(window).width()<=b.sm-1&&$("body").hasClass("sidebar-open")&&$("body").removeClass("sidebar-open")}),($.AdminLTE.options.sidebarExpandOnHover||$("body").hasClass("fixed")&&$("body").hasClass("sidebar-mini"))&&this.expandOnHover()},expandOnHover:function(){var a=this,b=$.AdminLTE.options.screenSizes.sm-1;$(".main-sidebar").hover(function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-collapse")&&$(window).width()>b&&a.expand()},function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-expanded-on-hover")&&$(window).width()>b&&a.collapse()})},expand:function(){$("body").removeClass("sidebar-collapse").addClass("sidebar-expanded-on-hover")},collapse:function(){$("body").hasClass("sidebar-expanded-on-hover")&&$("body").removeClass("sidebar-expanded-on-hover").addClass("sidebar-collapse")}},$.AdminLTE.tree=function(a){var b=this,c=$.AdminLTE.options.animationSpeed;$(a).on("click","li a",function(a){var d=$(this),e=d.next();if(e.is(".treeview-menu")&&e.is(":visible")&&!$("body").hasClass("sidebar-collapse"))e.slideUp(c,function(){e.removeClass("menu-open")}),e.parent("li").removeClass("active");else if(e.is(".treeview-menu")&&!e.is(":visible")){var f=d.parents("ul").first(),g=f.find("ul:visible").slideUp(c);g.removeClass("menu-open");var h=d.parent("li");e.slideDown(c,function(){e.addClass("menu-open"),f.find("li.active").removeClass("active"),h.addClass("active"),b.layout.fix()})}e.is(".treeview-menu")&&a.preventDefault()})},$.AdminLTE.controlSidebar={activate:function(){var a=this,b=$.AdminLTE.options.controlSidebarOptions,c=$(b.selector),d=$(b.toggleBtnSelector);d.on("click",function(d){d.preventDefault(),c.hasClass("control-sidebar-open")||$("body").hasClass("control-sidebar-open")?a.close(c,b.slide):a.open(c,b.slide)});var e=$(".control-sidebar-bg");a._fix(e),$("body").hasClass("fixed")?a._fixForFixed(c):$(".content-wrapper, .right-side").height() .box-body, > .box-footer, > form >.box-body, > form > .box-footer");c.hasClass("collapsed-box")?(a.children(":first").removeClass(b.icons.open).addClass(b.icons.collapse),d.slideDown(b.animationSpeed,function(){c.removeClass("collapsed-box")})):(a.children(":first").removeClass(b.icons.collapse).addClass(b.icons.open),d.slideUp(b.animationSpeed,function(){c.addClass("collapsed-box")}))},remove:function(a){var b=a.parents(".box").first();b.slideUp(this.animationSpeed)}}}if("undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-toggle='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef",yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC",olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)},a.fn.toggleBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.collapse,this);a.AdminLTE.boxWidget.collapse(b)},a.fn.removeBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.remove,this);a.AdminLTE.boxWidget.remove(b)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery); \ No newline at end of file diff --git a/web/static/img/app.png b/web/static/img/app.png new file mode 100644 index 0000000..4dbcbc2 Binary files /dev/null and b/web/static/img/app.png differ diff --git a/web/static/img/favicon.ico b/web/static/img/favicon.ico new file mode 100644 index 0000000..6a78719 Binary files /dev/null and b/web/static/img/favicon.ico differ diff --git a/web/static/img/home.png b/web/static/img/home.png new file mode 100644 index 0000000..385c04e Binary files /dev/null and b/web/static/img/home.png differ diff --git a/web/static/img/logo.png b/web/static/img/logo.png new file mode 100755 index 0000000..dfb6c29 Binary files /dev/null and b/web/static/img/logo.png differ diff --git a/web/static/img/logoname.png b/web/static/img/logoname.png new file mode 100644 index 0000000..0edf0c1 Binary files /dev/null and b/web/static/img/logoname.png differ diff --git a/web/static/img/profile.png b/web/static/img/profile.png new file mode 100644 index 0000000..caaef92 Binary files /dev/null and b/web/static/img/profile.png differ diff --git a/web/static/img/web.png b/web/static/img/web.png new file mode 100644 index 0000000..2a47a3a Binary files /dev/null and b/web/static/img/web.png differ diff --git a/web/static/img/workspace.png b/web/static/img/workspace.png new file mode 100644 index 0000000..092caa8 Binary files /dev/null and b/web/static/img/workspace.png differ diff --git a/web/static/js/plot_monitor.js b/web/static/js/plot_monitor.js new file mode 100755 index 0000000..5dcb2f7 --- /dev/null +++ b/web/static/js/plot_monitor.js @@ -0,0 +1,156 @@ +var mem_usedp = 0; +var cpu_usedp = 0; + + +function processMemData(data) +{ + mem_usedp = data.monitor.mem_use.usedp; + var usedp = data.monitor.mem_use.usedp; + var unit = data.monitor.mem_use.unit; + var quota = data.monitor.mem_use.quota; + var val = data.monitor.mem_use.val; + var out = "("+val+unit+"/"+quota+unit+")"; + $("#con_mem").html((usedp/0.01).toFixed(2)+"%
"+out); +} +function getMemY() +{ + return mem_usedp*100; +} +function processCpuData(data) +{ + cpu_usedp = data.monitor.cpu_use.usedp; + var val = data.monitor.cpu_use.val; + var unit = data.monitor.cpu_use.unit; + $("#con_cpu").html(val +" "+ unit); +} +function getCpuY() +{ + return cpu_usedp*100; +} + +function plot_graph(container,url,processData,getY) { + + //var container = $("#flot-line-chart-moving"); + + // Determine how many data points to keep based on the placeholder's initial size; + // this gives us a nice high-res plot while avoiding more than one point per pixel. + + var maximum = container.outerWidth() / 2 || 300; + + // + + var data = []; + + + + function getBaseData() { + + while (data.length < maximum) { + data.push(0) + } + + // zip the generated y values with the x values + + var res = []; + for (var i = 0; i < data.length; ++i) { + res.push([i, data[i]]) + } + + return res; + } + + function getData() { + + if (data.length) { + data = data.slice(1); + } + + if (data.length < maximum) { + $.post(url,{user:"root",key:"root"},processData,"json"); + var y = getY(); + data.push(y < 0 ? 0 : y > 100 ? 100 : y); + } + + // zip the generated y values with the x values + + var res = []; + for (var i = 0; i < data.length; ++i) { + res.push([i, data[i]]) + } + + return res; + } + + + + series = [{ + data: getBaseData(), + lines: { + fill: true + } + }]; + + + var plot = $.plot(container, series, { + grid: { + + color: "#999999", + tickColor: "#D4D4D4", + borderWidth:0, + minBorderMargin: 20, + labelMargin: 10, + backgroundColor: { + colors: ["#ffffff", "#ffffff"] + }, + margin: { + top: 8, + bottom: 20, + left: 20 + }, + markings: function(axes) { + var markings = []; + var xaxis = axes.xaxis; + for (var x = Math.floor(xaxis.min); x < xaxis.max; x += xaxis.tickSize * 2) { + markings.push({ + xaxis: { + from: x, + to: x + xaxis.tickSize + }, + color: "#fff" + }); + } + return markings; + } + }, + colors: ["#1ab394"], + xaxis: { + tickFormatter: function() { + return ""; + } + }, + yaxis: { + min: 0, + max: 110 + }, + legend: { + show: true + } + }); + + // Update the random dataset at 25FPS for a smoothly-animating chart + + setInterval(function updateRandom() { + series[0].data = getData(); + plot.setData(series); + plot.draw(); + }, 1000); + +} + +var host = window.location.host; + +var node_name = $("#node_name").html(); +var url = "http://" + host + "/monitor/vnodes/" + node_name; + +plot_graph($("#mem-chart"),url + "/mem_use",processMemData,getMemY); +plot_graph($("#cpu-chart"),url + "/cpu_use",processCpuData,getCpuY); diff --git a/web/static/js/plot_monitorReal.js b/web/static/js/plot_monitorReal.js new file mode 100755 index 0000000..5fd47b1 --- /dev/null +++ b/web/static/js/plot_monitorReal.js @@ -0,0 +1,197 @@ + +var used = 0; +var total = 0; +var idle = 0; +var disk_usedp = 0; +var count = 0; +var MB = 1024; + +function processMemData(data) +{ + used = data.monitor.meminfo.used; + total = data.monitor.meminfo.total; + var used2 = ((data.monitor.meminfo.used)/MB).toFixed(2); + var total2 = ((data.monitor.meminfo.total)/MB).toFixed(2); + var free2 = ((data.monitor.meminfo.free)/MB).toFixed(2); + $("#mem_used").html(used2); + $("#mem_total").html(total2); + $("#mem_free").html(free2); +} +function getMemY() +{ + if(total == 0) + return 0; + else + return (used/total)*100; +} +function processCpuData(data) +{ + idle = data.monitor.cpuinfo.idle; + var us = data.monitor.cpuinfo.user; + var sy = data.monitor.cpuinfo.system; + var wa = data.monitor.cpuinfo.iowait; + $("#cpu_user").html(us); + $("#cpu_system").html(sy); + $("#cpu_iowait").html(wa); + $("#cpu_idle").html(idle); +} +function getCpuY() +{ + count++; + //alert(idle); + if(count <= 3 && idle <= 10) + return 0; + else + return (100-idle); +} +function processDiskData(data) +{ + var vals = data.monitor.diskinfo; + disk_usedp = vals[0].usedp; + for(var idx = 0; idx < vals.length; ++idx) + { + var used = (vals[idx].used/MB/MB).toFixed(2); + var total = (vals[idx].total/MB/MB).toFixed(2); + var free = (vals[idx].free/MB/MB).toFixed(2); + var usedp = (vals[idx].percent); + var name = "#disk_" + (idx+1) + "_"; + $(name+"device").html(vals[idx].device); + $(name+"used").html(used); + $(name+"total").html(total); + $(name+"free").html(free); + $(name+"usedp").html(usedp); + } +} +function getDiskY() +{ + return disk_usedp; +} + +function plot_graph(container,url,processData,getY) { + + //var container = $("#flot-line-chart-moving"); + + // Determine how many data points to keep based on the placeholder's initial size; + // this gives us a nice high-res plot while avoiding more than one point per pixel. + + var maximum = container.outerWidth() / 2 || 300; + + // + + var data = []; + + + + function getBaseData() { + + while (data.length < maximum) { + data.push(0) + } + + // zip the generated y values with the x values + + var res = []; + for (var i = 0; i < data.length; ++i) { + res.push([i, data[i]]) + } + + return res; + } + + function getData() { + + if (data.length) { + data = data.slice(1); + } + + if (data.length < maximum) { + $.post(url,{user:"root",key:"unias"},processData,"json"); + var y = getY(); + data.push(y < 0 ? 0 : y > 100 ? 100 : y); + } + + // zip the generated y values with the x values + + var res = []; + for (var i = 0; i < data.length; ++i) { + res.push([i, data[i]]) + } + + return res; + } + + + + series = [{ + data: getBaseData(), + lines: { + fill: true + } + }]; + + + var plot = $.plot(container, series, { + grid: { + + color: "#999999", + tickColor: "#D4D4D4", + borderWidth:0, + minBorderMargin: 20, + labelMargin: 10, + backgroundColor: { + colors: ["#ffffff", "#ffffff"] + }, + margin: { + top: 8, + bottom: 20, + left: 20 + }, + markings: function(axes) { + var markings = []; + var xaxis = axes.xaxis; + for (var x = Math.floor(xaxis.min); x < xaxis.max; x += xaxis.tickSize * 2) { + markings.push({ + xaxis: { + from: x, + to: x + xaxis.tickSize + }, + color: "#fff" + }); + } + return markings; + } + }, + colors: ["#1ab394"], + xaxis: { + tickFormatter: function() { + return ""; + } + }, + yaxis: { + min: 0, + max: 110 + }, + legend: { + show: true + } + }); + + // Update the random dataset at 25FPS for a smoothly-animating chart + + setInterval(function updateRandom() { + series[0].data = getData(); + plot.setData(series); + plot.draw(); + }, 1000); + +} +var host = window.location.host; + +var com_ip = $("#com_ip").html(); +var url = "http://" + host + "/monitor/hosts/"+com_ip; + +plot_graph($("#mem-chart"), url + "/meminfo",processMemData,getMemY); +plot_graph($("#cpu-chart"), url + "/cpuinfo",processCpuData,getCpuY); +//plot_graph($("#disk-chart"), url + "/diskinfo",processDiskData,getDiskY); +$.post(url+"/diskinfo",{user:"root",key:"unias"},processDiskData,"json"); + diff --git a/web/templates/addCluster.html b/web/templates/addCluster.html new file mode 100644 index 0000000..04bf48c --- /dev/null +++ b/web/templates/addCluster.html @@ -0,0 +1,130 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Create Workspace{% endblock %} + +{% block css_src %} + +{% endblock %} + +{% block panel_title %}Workspace Info{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +
+
+
+
+

Workspace Add

+ +
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + {% for image in images['private'] %} + + + + + + + + {% endfor %} + {% for p_user,p_images in images['public'].items() %} + {% for image in p_images %} + + + + + + + + {% endfor %} + {% endfor %} + +
ImageNameTypeOwnerDescriptionChoose
base
public
dockletA base image for you
{{image['name']}}
{{"private"}}
{{user}}{{image['description']}}
{{image['name']}}
{{"public"}}
{{p_user}}{{image['description']}}
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+ +
+ +{% endblock %} + +{% block script_src %} + + + + + + + + + + + + + + + + + + +{% endblock %} diff --git a/web/templates/admin.html b/web/templates/admin.html new file mode 100644 index 0000000..de53b83 --- /dev/null +++ b/web/templates/admin.html @@ -0,0 +1,173 @@ +{% extends "base_AdminLTE.html"%} +{% block title %}Docklet | Admin{% endblock %} + +{% block panel_title %}Admin{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block css_src %} + + + + + +{% endblock %} + + +{% block content %} +
+
+
+
+

Quota

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + +
IDNameCPUMemoryImageQuantityLifeCycleCommand
+
+
+
+
+ +{% endblock %} + +{% block script_src %} + + + + + +{% endblock %} diff --git a/web/templates/base_AdminLTE.html b/web/templates/base_AdminLTE.html new file mode 100644 index 0000000..51f6afc --- /dev/null +++ b/web/templates/base_AdminLTE.html @@ -0,0 +1,290 @@ + + + + + + {% block title %}Docklet | Dashboard{% endblock %} + + + + + + + + + + + + + + + + + + + + + + + {%block css_src %}{% endblock %} + + + + + +
+ + +
+ + + + + + +
+ + + + +
+ +
+

+ {% block panel_title %}Dashboard{% endblock %} +

+ {% block panel_list %} + + {% endblock %} +
+ +
+ + {% block content %} + {% endblock %} + +
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + +{% if mysession['status'] == 'init' %} + + +{% endif %} + +{% if mysession['status'] == 'applying' %} + + +{% endif %} + + +{% block script_src %} +{% endblock %} + + + diff --git a/web/templates/config.html b/web/templates/config.html new file mode 100755 index 0000000..6c74c04 --- /dev/null +++ b/web/templates/config.html @@ -0,0 +1,315 @@ +{% extends "base_AdminLTE.html"%} + + + +{% block title %}Docklet | Config{% endblock %} + +{% block panel_title %}Config{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block css_src %} + +{% endblock %} + + +{% block content %} +{% for clustername, clusterinfo in clusters.items() %} +
+
+
+
+

WorkSpace Name: {{ clustername }}

+ +
+ + +
+
+
+
+
+
+
+

VCLUSTER

+
create_time:{{clusterinfo['create_time']}}      start_time:{{clusterinfo['start_time']}}
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + {% for container in clusterinfo['containers'] %} + + + + + + {% if clusterinfo['status'] == 'stopped' %} + + {% else %} + + {% endif %} + + + + {% if container['containername'][-2:] == '-0' %} + + {% else %} + + {% endif %} + + + + + {% endfor %} + +
Node IDNode NameIP AddressStatusImageSaveDelete
{{ loop.index }}{{ container['containername'] }}{{ container['ip'] }}
Stopped
Running
{{ container['image'] }}Delete
+
+
+
+
+
+
+
+
+
+

SERVICE

+
{{ clusterinfo['proxy_url'] }}
+ +
+ + +
+
+
+
+ {% if 'proxy_ip' in clusterinfo %} +

ip:port: + +

+ {% else %} +

ip:port: + +

+ {% endif %} +
+
+
+
+
+
+
+
+
+{% endfor %} +
+
+
+
+

Image Info

+
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + {% for image in images['private'] %} + + + + + + + {% if image['isshared'] == 'false' %} + + + {% else %} + + + {% endif %} + + {% endfor %} + {% for p_user,p_images in images['public'].items() %} + {% for image in p_images %} + + + + + + + + {% if p_user == mysession['username'] %} + + {% else %} + + {% endif %} + + {% endfor %} + {% endfor %} + +
ImageNameTypeOwnerCreateTimeDescriptionStatusOperation
base
public
docklet2015-01-01 00:00:00A Base Image For You
{{image['name']}}
{{"private"}}
{{mysession['username']}}{{image['time']}}{{image['description']}}
unshared
+ + +
shared
+ + +
{{image['name']}}
{{"public"}}
{{p_user}}{{image['time']}}{{image['description']}}
+
+
+
+
+ +{% endblock %} + +{% block script_src %} + + + + + + + +{% endblock %} diff --git a/web/templates/dashboard.html b/web/templates/dashboard.html new file mode 100755 index 0000000..1c0a77e --- /dev/null +++ b/web/templates/dashboard.html @@ -0,0 +1,89 @@ +{% extends "base_AdminLTE.html"%} +{% block title %}Docklet | Dashboard{% endblock %} + +{% block panel_title %}Dashboard{% endblock %} + +{% block panel_list %} + +{% endblock %} +{% block content %} +
+
+
+
+

Workspaces

+ +
+ + +
+
+
+ +

+ +

+ + + + + + + + + + + + {% for cluster in clusters %} + + + + {% if cluster['status'] == 'running' %} + + + + {% else %} + + + + {% endif %} + + {% endfor %} + +
IDNameStatusOperationWorkSpace
{{ cluster['id'] }}{{ cluster['name'] }}
Running
+ + + + +
Stopped
+ + + + +
+ +
+
+
+
+ +{% endblock %} +{% block script_src %} + + +{% endblock %} diff --git a/web/templates/dashboard_guest.html b/web/templates/dashboard_guest.html new file mode 100644 index 0000000..0f71040 --- /dev/null +++ b/web/templates/dashboard_guest.html @@ -0,0 +1,329 @@ + + + + + + Docklet | Dashboard + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + + + + + +
+ + + + +
+ +
+

+ Dashboard +

+ + + +
+ +
+ + +
+
+
+
+

Workspaces

+ +
+ + +
+
+
+ +

+ +

+ + + + + + + + + + + + + + + + + + + +
IDNameStatusOperationWorkSpace
1guest-1-0
Running
+ + + + +
+ +
+
+
+
+ + +
+ +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + +{% if mysession['status'] == 'init' %} + + +{% endif %} + +{% if mysession['status'] == 'applying' %} + + +{% endif %} + + + + + diff --git a/web/templates/error.html b/web/templates/error.html new file mode 100644 index 0000000..865ff80 --- /dev/null +++ b/web/templates/error.html @@ -0,0 +1,10 @@ +{% extends "base_AdminLTE.html"%} +{% block title %}Docklet | Error{% endblock %} + +{% block panel_title %}Error{% endblock %} + +{% block panel_list %}{% endblock %} + +{% block content %} +
{{message}}
+{% endblock %} diff --git a/web/templates/error/401.html b/web/templates/error/401.html new file mode 100755 index 0000000..78301ea --- /dev/null +++ b/web/templates/error/401.html @@ -0,0 +1,30 @@ +{% extends "base_AdminLTE.html"%} + + +{% block title %}Docklet | Error{% endblock %} + +{% block panel_title %}401 Error Page{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} + +
+

401

+ +
+


Unauthorized Action

+ +

+ Sorry, but you did not have the authorizaion for that action, you can go back to + dashboard or log out +

+
+
+{% endblock %} diff --git a/web/templates/error/500.html b/web/templates/error/500.html new file mode 100755 index 0000000..56c27db --- /dev/null +++ b/web/templates/error/500.html @@ -0,0 +1,30 @@ +{% extends "base_AdminLTE.html"%} + + +{% block title %}Docklet | Error{% endblock %} + +{% block panel_title %}500 Error Page{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} + +
+

500

+ +
+


Internal Server Error

+ +

+ The server encountered something unexpected that didn't allow it to complete the request. We apologize.You can go back to + dashboard or log out +

+
+
+{% endblock %} diff --git a/web/templates/home.html b/web/templates/home.html new file mode 100755 index 0000000..e755fa2 --- /dev/null +++ b/web/templates/home.html @@ -0,0 +1,113 @@ + + + + + + + + + Docklet | Home + + + + + + + + + + + + + + + + + +
+
+ +
+ +
+
+ +
+
+

Workspace = Cluster+Service+Data

+

Package service and data based on virtual cluster as virtual compute environment for your work. This is your Workspace !

+
+
+ +
+
+

Click and Go

+

Distributed or single node ? Never mind ! + Click it just like start an app on your smart phone, and your workspace is + ready for you.

+
+
+ +
+ +
+
+
+ +
+
+

All in Web

+

All you need is a web browser. + Compute in web, code in web, plot in web, anything in web ! + You can get to work anytime and anywhere by internet.

+
+ +
+
+
+

Now,   jupyter / python3 / matplotlib / sklearn /scipy / numpy / pandas / latex is ready for you

+

And,   more workspaces are coming for your data processing / data mining / machine learning work

+
+ +     + +
+
+ + + + +
+

Copyright© 2016 UniAS@ SEI, PKU

+
+ +
+ + + + + + + + diff --git a/web/templates/image_description.html b/web/templates/image_description.html new file mode 100755 index 0000000..7522fdd --- /dev/null +++ b/web/templates/image_description.html @@ -0,0 +1,10 @@ +{% extends "base_AdminLTE.html"%} +{% block title %}Docklet | Description{% endblock %} + +{% block panel_title %}Description{% endblock %} + +{% block panel_list %}{% endblock %} + +{% block content %} +
{{description}}
+{% endblock %} diff --git a/web/templates/listcontainer.html b/web/templates/listcontainer.html new file mode 100644 index 0000000..0b3da3b --- /dev/null +++ b/web/templates/listcontainer.html @@ -0,0 +1,102 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Container{% endblock %} + +{% block panel_title %}ContainerInfo{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +
+
+
+
+

Cluster Name: {{ clustername }}

+ +
+ + +
+
+
+

+ +

+ + + + + + + + + + + + + + + + {% for container in containers %} + + + + + + {% if status == 'stopped' %} + + {% else %} + + {% endif %} + + + + + + + + + + + {% endfor %} + +
Node IDNode NameIP AddressStatusLast SaveImageDetailFlushSave
{{ loop.index }}{{ container['containername'] }}{{ container['ip'] }}
Stopped
Running
{{ container['lastsave'] }}{{ container['image'] }}DetailFlush
+
+
+
+
+ +{% endblock %} diff --git a/web/templates/login.html b/web/templates/login.html new file mode 100755 index 0000000..cfc7a55 --- /dev/null +++ b/web/templates/login.html @@ -0,0 +1,71 @@ + + + + + + + Docklet | Login + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/templates/monitor/hosts.html b/web/templates/monitor/hosts.html new file mode 100644 index 0000000..afbe6af --- /dev/null +++ b/web/templates/monitor/hosts.html @@ -0,0 +1,145 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Hosts{% endblock %} + +{% block panel_title %}Hosts Info{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +
+
+
+
+

All Hosts Info

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + {% for phym in machines %} + + + + {% if phym['status'] == 'STOPPED' %} + + {% else %} + + {% endif %} + + + + + + + {% endfor %} + +
NOIP AddressStatusNodes runningCpu usedMem usedDisk usedSummary
{{ loop.index }}{{ phym['ip'] }}
Stopped
Running
+ / + {{ phym['containers']['total'] }} + ------Realtime
+ +
+
+
+
+ +{% endblock %} + +{% block script_src %} + +{% endblock %} diff --git a/web/templates/monitor/hostsConAll.html b/web/templates/monitor/hostsConAll.html new file mode 100644 index 0000000..a420e9f --- /dev/null +++ b/web/templates/monitor/hostsConAll.html @@ -0,0 +1,136 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Hosts{% endblock %} + +{% block panel_title %}Node list for {{ com_ip }}{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +
+
+
+
+

Total Nodes

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + {% for container in containerslist %} + + + + {% if container['State'] == 'STOPPED' %} + + + + {% else %} + + + + {% endif %} + + + + + {% endfor %} + +
NONameStatePIDIP AddressCpu usedMem usedSummary
{{ loop.index }}{{ container['Name'] }}
Stopped
----
Running
{{ container['PID'] }}{{ container['IP'] }}----Realtime
+ +
+
+
+
+ +{% endblock %} + +{% block script_src %} + +{% endblock %} diff --git a/web/templates/monitor/hostsRealtime.html b/web/templates/monitor/hostsRealtime.html new file mode 100644 index 0000000..aae07ee --- /dev/null +++ b/web/templates/monitor/hostsRealtime.html @@ -0,0 +1,289 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Hosts{% endblock %} + +{% block panel_title %}Summary for
{{ com_ip }}
{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block css_src %} + +{% endblock %} + +{% block content %} +
+
+
+
+

CPU info

+ +
+ + +
+
+
+ + + + + + + + + + + + + {% for processor in processors %} + + + + + + + + + {% endfor %} + +
Processor IDModel namephysical idcore idcpu MHzcache size
{{ processor['processor'] }}{{ processor['model name']}}{{ processor['physical id']}}{{ processor['core id']}}{{ processor['cpu MHz']}}{{ processor['cache size']}}
+ +
+
+
+
+ +
+
+
+
+

OS info

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
OS name{{ OSinfo['platform']}}
OS node name{{ OSinfo['node']}}
OS kernel release{{ OSinfo['release']}}
OS kernel version{{ OSinfo['version']}}
OS kernel machine{{ OSinfo['machine']}}
+ +
+
+
+
+ +
+
+
+
+

Cpu and Memory Status

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cpu(%)Memory(MB)
usersystemiowaitidleusedfreetotal
--------------
+
+
+
+
+ +
+
+
+
+

Disk Status

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + {% for diskinfo in diskinfos %} + + + + + + + + {% endfor %} + +
Disk info
deviceused(MB)free(MB)total(MB)used percent(%)
----------
+ +
+
+
+
+ +
+
+
+
+

Memory Used(%):

+ +
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+

CPU Used(%):

+ +
+ + +
+
+
+ +
+
+
+
+
+
+
+ + + + +{% endblock %} + +{% block script_src %} + + + + + + + + + +{% endblock %} diff --git a/web/templates/monitor/monitorUserAll.html b/web/templates/monitor/monitorUserAll.html new file mode 100644 index 0000000..1b436ab --- /dev/null +++ b/web/templates/monitor/monitorUserAll.html @@ -0,0 +1,65 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | MonitorUser{% endblock %} + +{% block panel_title %}Users Info{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +
+
+
+
+

All Users Info

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + {% for user in userslist %} + + + + + + + + + + + {% endfor %} + +
NONameRunning/Total ClustersRunning/Total ContainersRegister TimeLast LoginFrequencyDetail
{{ loop.index }}{{ user['name'] }}{{ user['clustercnt']['clurun'] }}/{{ user['clustercnt']['clutotal'] }}{{ user['clustercnt']['conrun'] }}/{{ user['clustercnt']['contotal'] }}------Clusters
+ +
+
+
+
+{% endblock %} diff --git a/web/templates/monitor/monitorUserCluster.html b/web/templates/monitor/monitorUserCluster.html new file mode 100644 index 0000000..4d983be --- /dev/null +++ b/web/templates/monitor/monitorUserCluster.html @@ -0,0 +1,74 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Monitor{% endblock %} + +{% block panel_title %}NodeInfo for {{ muser }}{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +{% for cluster in clusters %} +
+
+
+
+

Cluster Name: {{ cluster }}

+ +
+ + +
+
+
+ + + + + + + + + + + + + + {% for container in containers[cluster]['containers'] %} + + + + + + {% if containers[cluster]['status'] == 'stopped' %} + + {% else %} + + {% endif %} + + + + + {% endfor %} + +
Node IDNode NameIP AddressStatusCreate Timedetail
{{ loop.index }}{{ container['containername'] }}{{ container['ip'] }}
Stopped
Running
xxxxxDetail
+ +
+
+
+
+ +{% endfor %} +{% endblock %} diff --git a/web/templates/monitor/status.html b/web/templates/monitor/status.html new file mode 100644 index 0000000..24e860c --- /dev/null +++ b/web/templates/monitor/status.html @@ -0,0 +1,136 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Status{% endblock %} + +{% block panel_title %}Workspace VCluster Status{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} +{% for cluster in clusters %} +
+
+
+
+

VCluster Name: {{ cluster }}

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + {% for container in containers[cluster]['containers'] %} + + + + + + {% if containers[cluster]['status'] == 'stopped' %} + + {% else %} + + {% endif %} + + + + + + {% endfor %} + +
Node IDNode NameIP AddressStatusCpu usedMem usedSummary
{{ loop.index }}{{ container['containername'] }}{{ container['ip'] }}
Stopped
Running
----Realtime
+ +
+
+
+
+ +{% endfor %} +{% endblock %} + +{% block script_src %} + +{% endblock %} diff --git a/web/templates/monitor/statusRealtime.html b/web/templates/monitor/statusRealtime.html new file mode 100644 index 0000000..42b1e15 --- /dev/null +++ b/web/templates/monitor/statusRealtime.html @@ -0,0 +1,122 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Node Summary{% endblock %} + +{% block panel_title %}Summary for
{{ node_name }}
{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block css_src %} + +{% endblock %} + +{% block content %} +
+
+
+
+

Current Status

+ +
+ + +
+
+
+ + + + + + + + + + + + + + {% if container['State'] == 'STOPPED' %} + + + {% else %} + + + {% endif %} + + + + +
NameStateIP AddressCPU UseMem Use
{{ container['Name'] }}
Stopped
--
Running
{{ container['IP'] }}----
+ +
+
+
+
+
+
+
+
+

Memory Used(%):

+ +
+ + +
+
+
+ +
+
+
+
+
+
+
+
+
+

CPU Used(%):

+ +
+ + +
+
+
+ +
+
+
+
+
+
+
+{% endblock %} + +{% block script_src %} + + + + + + + + + + +{% endblock %} diff --git a/web/templates/opfailed.html b/web/templates/opfailed.html new file mode 100644 index 0000000..1e848af --- /dev/null +++ b/web/templates/opfailed.html @@ -0,0 +1,24 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Failed{% endblock %} + +{% block panel_title %}Failed{% endblock %} + +{% block panel_list %} + +{% endblock %} +{% block content %} + +{% endblock %} diff --git a/web/templates/opsuccess.html b/web/templates/opsuccess.html new file mode 100644 index 0000000..256e504 --- /dev/null +++ b/web/templates/opsuccess.html @@ -0,0 +1,24 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Success{% endblock %} + +{% block panel_title %}Success{% endblock %} + +{% block panel_list %} + +{% endblock %} +{% block content %} + +{% endblock %} diff --git a/web/templates/saveconfirm.html b/web/templates/saveconfirm.html new file mode 100644 index 0000000..9ffdfbd --- /dev/null +++ b/web/templates/saveconfirm.html @@ -0,0 +1,35 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Confirm{% endblock %} + +{% block panel_title %}Confirm{% endblock %} + +{% block css_src %} +.hide { display:none; } +{% endblock %} + +{% block panel_list %} + +{% endblock %} + + +{% block content %} +
+
+ + + +
+
+ + +
+
+{% endblock %} diff --git a/web/templates/user/activate.html b/web/templates/user/activate.html new file mode 100644 index 0000000..afeae64 --- /dev/null +++ b/web/templates/user/activate.html @@ -0,0 +1,73 @@ + + + + + + Docklet | Login + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/templates/user/info.html b/web/templates/user/info.html new file mode 100644 index 0000000..f373d41 --- /dev/null +++ b/web/templates/user/info.html @@ -0,0 +1,256 @@ +{% extends 'base_AdminLTE.html' %} + +{% block title %}Docklet | Information Modify{% endblock %} + +{% block css_src %} + +{% endblock %} + +{% block panel_title %}Detail for User Infomation{% endblock %} + +{% block panel_list %} + +{% endblock %} + + +{% block content %} +
+
+
+
+

User Info

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
User Name{{ info['username'] }}
Nickname{{ info['nickname'] }}
Description{{ info['description'] }}
Truename{{ info['truename'] }}
Status{{ info['status'] }}
E-mail{{ info['e_mail'] }}
Department{{ info['department'] }}
ID Number{{ info['student_number'] }}
Telephone{{ info['tel'] }}
+
+
+
+ +
+ +{% endblock %} + +{% block script_src %} + + + + +{% endblock %} diff --git a/web/templates/user/mailservererror.html b/web/templates/user/mailservererror.html new file mode 100644 index 0000000..b4b7d1f --- /dev/null +++ b/web/templates/user/mailservererror.html @@ -0,0 +1,30 @@ +{% extends "base_AdminLTE.html"%} + + +{% block title %}Docklet | Error{% endblock %} + +{% block panel_title %}500 Error Page{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block content %} + +
+

500

+ +
+


Internal Server Error

+ +

+ Please examine your mail server config(now exim4).You can go back to + dashboard or log out +

+
+
+{% endblock %} diff --git a/web/templates/user_list.html b/web/templates/user_list.html new file mode 100644 index 0000000..c3f31bd --- /dev/null +++ b/web/templates/user_list.html @@ -0,0 +1,273 @@ +{% extends "base_AdminLTE.html"%} +{% block title %}Docklet | UserList{% endblock %} + +{% block panel_title %}UserList{% endblock %} + +{% block panel_list %} + +{% endblock %} + +{% block css_src %} + + + + + + +{% endblock %} + + +{% block content %} +
+
+
+
+

User List

+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + +
IDUserNameE_mailTelRegisterDateStatusGroupCommand
+ +
+
+
+{% endblock %} + +{% block script_src %} + + + + + +{% endblock %} diff --git a/web/web.py b/web/web.py new file mode 100755 index 0000000..5911d05 --- /dev/null +++ b/web/web.py @@ -0,0 +1,448 @@ +#!/usr/bin/python3 +import json +import os +import getopt + +import sys, inspect +this_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) +src_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,"..", "src"))) +if src_folder not in sys.path: + sys.path.insert(0, src_folder) + +# must first init loadenv +import tools, env +config = env.getenv("CONFIG") +tools.loadenv(config) + +from webViews.log import initlogging +initlogging("docklet-web") +from webViews.log import logger + +from flask import Flask, request, session, render_template, redirect, send_from_directory, make_response, url_for, abort +from webViews.dashboard import dashboardView +from webViews.user.userlist import userlistView, useraddView, usermodifyView, groupaddView, userdataView, userqueryView +from webViews.user.userinfo import userinfoView +from webViews.user.userActivate import userActivateView +from webViews.user.grouplist import grouplistView, groupqueryView, groupdetailView, groupmodifyView +from functools import wraps +from webViews.dockletrequest import dockletRequest +from webViews.cluster import * +from webViews.admin import * +from webViews.monitor import * +from webViews.authenticate.auth import login_required, administration_required,activated_required +from webViews.authenticate.register import registerView +from webViews.authenticate.login import loginView, logoutView +import webViews.dockletrequest +from webViews import cookie_tool + + + + + +external_login = env.getenv('EXTERNAL_LOGIN') +#default config +external_login_url = '/external_auth/' +external_login_callback_url = '/external_auth_callback/' +if (external_login == 'True'): + sys.path.insert(0, os.path.realpath(os.path.abspath(os.path.join(this_folder,"../src", "plugin")))) + import external_generate + from webViews.authenticate.login import external_loginView, external_login_callbackView + external_login_url = external_generate.external_login_url + external_login_callback_url = external_generate.external_login_callback_url + + +app = Flask(__name__) + + + +@app.route("/", methods=['GET']) +def home(): + return render_template('home.html') + +@app.route("/login/", methods=['GET', 'POST']) +def login(): + return loginView.as_view() + +@app.route(external_login_url, methods=['GET']) +def external_login_func(): + try: + return external_loginView.as_view() + except: + abort(404) + +@app.route(external_login_callback_url, methods=['GET']) +def external_login_callback(): + try: + return external_login_callbackView.as_view() + except: + abort(404) + +@app.route("/logout/", methods=["GET"]) +@login_required +def logout(): + return logoutView.as_view() + +@app.route("/register/", methods=['GET', 'POST']) +@administration_required +#now forbidden,only used by SEI & PKU Staffs and students. +#can be used by admin for testing +def register(): + return registerView.as_view() + + + +@app.route("/activate/", methods=['GET', 'POST']) +@login_required +def activate(): + return userActivateView.as_view() + +@app.route("/dashboard/", methods=['GET']) +@login_required +def dashboard(): + return dashboardView.as_view() + +@app.route("/dashboard_guest/", methods=['GET']) +def dashboard_guest(): + resp = make_response(dashboard_guestView.as_view()) + resp.set_cookie('guest-cookie', cookie_tool.generate_cookie('guest', app.secret_key)) + return resp + +@app.route("/document/", methods=['GET']) +def redirect_dochome(): + return redirect("http://docklet.unias.org/userguide") + +@app.route("/config/", methods=['GET']) +@login_required +def config(): + return configView.as_view() + + +@app.route("/workspace/create/", methods=['GET']) +@activated_required +def addCluster(): + return addClusterView.as_view() + +@app.route("/workspace/list/", methods=['GET']) +@login_required +def listCluster(): + return listClusterView.as_view() + +@app.route("/workspace/add/", methods=['POST']) +@login_required +def createCluster(): + createClusterView.clustername = request.form["clusterName"] + createClusterView.image = request.form["image"] + return createClusterView.as_view() + +@app.route("/workspace/scaleout//", methods=['POST']) +@login_required +def scaleout(clustername): + scaleoutView.image = request.form["image"] + scaleoutView.clustername = clustername + return scaleoutView.as_view() + +@app.route("/workspace/scalein///", methods=['GET']) +@login_required +def scalein(clustername,containername): + scaleinView.clustername = clustername + scaleinView.containername = containername + return scaleinView.as_view() + +@app.route("/workspace/start//", methods=['GET']) +@login_required +def startClustet(clustername): + startClusterView.clustername = clustername + return startClusterView.as_view() + +@app.route("/workspace/stop//", methods=['GET']) +@login_required +def stopClustet(clustername): + stopClusterView.clustername = clustername + return stopClusterView.as_view() + +@app.route("/workspace/delete//", methods=['GET']) +@login_required +def deleteClustet(clustername): + deleteClusterView.clustername = clustername + return deleteClusterView.as_view() + +@app.route("/workspace/detail//", methods=['GET']) +@login_required +def detailCluster(clustername): + detailClusterView.clustername = clustername + return detailClusterView.as_view() + +@app.route("/workspace/flush///", methods=['GET']) +@login_required +def flushCluster(clustername,containername): + flushClusterView.clustername = clustername + flushClusterView.containername = containername + return flushClusterView.as_view() + +@app.route("/workspace/save///", methods=['POST']) +@login_required +def saveImage(clustername,containername): + saveImageView.clustername = clustername + saveImageView.containername = containername + saveImageView.isforce = "false" + saveImageView.imagename = request.form['ImageName'] + saveImageView.description = request.form['description'] + return saveImageView.as_view() + +@app.route("/workspace/save///force/", methods=['POST']) +@login_required +def saveImage_force(clustername,containername): + saveImageView.clustername = clustername + saveImageView.containername = containername + saveImageView.isforce = "true" + saveImageView.imagename = request.form['ImageName'] + saveImageView.description = request.form['description'] + return saveImageView.as_view() + +@app.route("/addproxy//", methods=['POST']) +@login_required +def addproxy(clustername): + addproxyView.clustername = clustername + addproxyView.ip = request.form['proxy_ip'] + addproxyView.port = request.form['proxy_port'] + return addproxyView.as_view() + +@app.route("/deleteproxy//", methods=['GET']) +@login_required +def deleteproxy(clustername): + deleteproxyView.clustername = clustername + return deleteproxyView.as_view() + +@app.route("/image/description//", methods=['GET']) +@login_required +def descriptionImage(image): + descriptionImageView.image = image + return descriptionImageView.as_view() + +@app.route("/image/share//", methods=['GET']) +@login_required +def shareImage(image): + shareImageView.image = image + return shareImageView.as_view() + +@app.route("/image/unshare//", methods=['GET']) +@login_required +def unshareImage(image): + unshareImageView.image = image + return unshareImageView.as_view() + +@app.route("/image/delete//", methods=['GET']) +@login_required +def deleteImage(image): + deleteImageView.image = image + return deleteImageView.as_view() + +@app.route("/hosts/", methods=['GET']) +@administration_required +def hosts(): + return hostsView.as_view() + +@app.route("/hosts//", methods=['GET']) +@administration_required +def hostsRealtime(com_ip): + hostsRealtimeView.com_ip = com_ip + return hostsRealtimeView.as_view() + +@app.route("/hosts//containers/", methods=['GET']) +@administration_required +def hostsConAll(com_ip): + hostsConAllView.com_ip = com_ip + return hostsConAllView.as_view() + +@app.route("/vclusters/", methods=['GET']) +@login_required +def status(): + return statusView.as_view() + +@app.route("/vclusters///", methods=['GET']) +@login_required +def statusRealtime(vcluster_name,node_name): + statusRealtimeView.node_name = node_name + return statusRealtimeView.as_view() + +@app.route("/monitor/hosts//", methods=['POST']) +@app.route("/monitor/vnodes//", methods=['POST']) +@login_required +def monitor_request(comid,infotype): + data = { + "user": session['username'] + } + result = dockletRequest.post(request.path, data) + return json.dumps(result) + +@app.route("/monitor/User/", methods=['GET']) +@administration_required +def monitorUserAll(): + return monitorUserAllView.as_view() + + + + +@app.route("/user/list/", methods=['GET', 'POST']) +@administration_required +def userlist(): + return userlistView.as_view() + +@app.route("/group/list/", methods=['POST']) +@administration_required +def grouplist(): + return grouplistView.as_view() + +@app.route("/group/detail/", methods=['POST']) +@administration_required +def groupdetail(): + return groupdetailView.as_view() + +@app.route("/group/query/", methods=['POST']) +@administration_required +def groupquery(): + return groupqueryView.as_view() + +@app.route("/group/modify/", methods=['POST']) +@administration_required +def groupmodify(): + return groupmodifyView.as_view() + +@app.route("/user/data/", methods=['GET', 'POST']) +@administration_required +def userdata(): + return userdataView.as_view() + +@app.route("/user/add/", methods=['POST']) +@administration_required +def useradd(): + return useraddView.as_view() + +@app.route("/user/modify/", methods=['POST']) +@administration_required +def usermodify(): + return usermodifyView.as_view() + +@app.route("/group/add/", methods=['POST']) +@administration_required +def groupadd(): + return groupaddView.as_view() + +@app.route("/user/info/", methods=['GET', 'POST']) +@login_required +def userinfo(): + return userinfoView.as_view() + +@app.route("/user/query/", methods=['GET', 'POST']) +@administration_required +def userquery(): + return userqueryView.as_view() + + +@app.route("/admin/", methods=['GET', 'POST']) +@administration_required +def adminpage(): + return adminView.as_view() + +@app.route('/index/', methods=['GET']) +def jupyter_control(): + return redirect('/dashboard/') + +# for download basefs.tar.bz +# remove, not the function of docklet +# should download it from a http server +#@app.route('/download/basefs', methods=['GET']) +#def download(): + #fsdir = env.getenv("FS_PREFIX") + #return send_from_directory(fsdir+'/local', 'basefs.tar.bz', as_attachment=True) + +# jupyter auth APIs +@app.route('/jupyter/', methods=['GET']) +def jupyter_prefix(): + path = request.args.get('next') + if path == None: + return redirect('/login/') + return redirect('/login/'+'?next='+path) + +@app.route('/jupyter/home/', methods=['GET']) +def jupyter_home(): + return redirect('/dashboard/') + +@app.route('/jupyter/login/', methods=['GET', 'POST']) +def jupyter_login(): + return redirect('/login/') + +@app.route('/jupyter/logout/', methods=['GET']) +def jupyter_logout(): + return redirect('/logout/') + +@app.route('/jupyter/authorizations/cookie///', methods=['GET']) +def jupyter_auth(cookie_name, cookie_content): + username = cookie_tool.parse_cookie(cookie_content, app.secret_key) + if username == None: + resp = make_response('cookie auth failed') + resp.status_code = 404 + return resp + return json.dumps({'name': username}) + +@app.errorhandler(401) +def not_authorized(error): + if "username" in session: + return render_template('error/401.html', mysession = session) + else: + return redirect('/login/') + +@app.errorhandler(500) +def internal_server_error(error): + if "username" in session: + return render_template('error/500.html', mysession = session) + else: + return redirect('/login/') +if __name__ == '__main__': + ''' + to generate a secret_key + + from base64 import b64encode + from os import urandom + + secret_key = urandom(24) + secret_key = b64encode(secret_key).decode('utf-8') + + ''' + logger.info('Start Flask...:') + try: + secret_key_file = open(env.getenv('FS_PREFIX') + '/local/web_secret_key.txt') + app.secret_key = secret_key_file.read() + secret_key_file.close() + except: + from base64 import b64encode + from os import urandom + secret_key = urandom(24) + secret_key = b64encode(secret_key).decode('utf-8') + app.secret_key = secret_key + secret_key_file = open(env.getenv('FS_PREFIX') + '/local/web_secret_key.txt', 'w') + secret_key_file.write(secret_key) + secret_key_file.close() + + os.environ['APP_KEY'] = app.secret_key + runcmd = sys.argv[0] + app.runpath = runcmd.rsplit('/', 1)[0] + + webip = "0.0.0.0" + webport = env.getenv("WEB_PORT") + + webViews.dockletrequest.endpoint = 'http://%s:%d' % (env.getenv('MASTER_IP'), env.getenv('MASTER_PORT')) + + + try: + opts, args = getopt.getopt(sys.argv[1:], "i:p:", ["ip=", "port="]) + except getopt.GetoptError: + print ("%s -i ip -p port" % sys.argv[0]) + sys.exit(2) + for opt, arg in opts: + if opt in ("-i", "--ip"): + webip = arg + elif opt in ("-p", "--port"): + webport = int(arg) + + app.run(host = webip, port = webport, debug = True, threaded=True) diff --git a/web/webViews/admin.py b/web/webViews/admin.py new file mode 100755 index 0000000..a043354 --- /dev/null +++ b/web/webViews/admin.py @@ -0,0 +1,14 @@ +from flask import session +from webViews.view import normalView +from webViews.dockletrequest import dockletRequest +from webViews.dashboard import * +import time, re + +class adminView(normalView): + template_path = "admin.html" + + @classmethod + def get(self): + groups = dockletRequest.post('/user/groupNameList/')["groups"] + return self.render(self.template_path, groups = groups) + diff --git a/web/webViews/authenticate/auth.py b/web/webViews/authenticate/auth.py new file mode 100644 index 0000000..730ee2c --- /dev/null +++ b/web/webViews/authenticate/auth.py @@ -0,0 +1,60 @@ +from flask import session, request, abort, redirect +from functools import wraps + + +def login_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if request.method == 'POST' : + if not is_authenticated(): + abort(401) + else: + return func(*args, **kwargs) + else: + if not is_authenticated(): + return redirect("/login/" + "?next=" + request.path) + else: + return func(*args, **kwargs) + + return wrapper + +def administration_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if not is_admin(): + abort(401) + else: + return func(*args, **kwargs) + + + return wrapper + +def activated_required(func): + @wraps(func) + def wrapper(*args, **kwargs): + if not is_activated(): + abort(401) + else: + return func(*args, **kwargs) + + + return wrapper + +def is_authenticated(): + if "username" in session: + return True + else: + return False +def is_admin(): + if not "username" in session: + return False + if not (session['usergroup'] == 'root' or session['usergroup'] == 'admin'): + return False + return True + +def is_activated(): + if not "username" in session: + return False + if not (session['status']=='normal'): + return False + return True diff --git a/web/webViews/authenticate/login.py b/web/webViews/authenticate/login.py new file mode 100755 index 0000000..bdf728f --- /dev/null +++ b/web/webViews/authenticate/login.py @@ -0,0 +1,138 @@ +from webViews.view import normalView +from webViews.authenticate.auth import is_authenticated +from webViews.dockletrequest import dockletRequest +from flask import redirect, request, render_template, session, make_response +from webViews import cookie_tool + +import hashlib +#from suds.client import Client + +import os, sys, inspect +this_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) +src_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,"../../..", "src"))) +if src_folder not in sys.path: + sys.path.insert(0, src_folder) + +import env + +if (env.getenv('EXTERNAL_LOGIN') == 'True'): + sys.path.insert(0, os.path.realpath(os.path.abspath(os.path.join(this_folder,"../../../src", "plugin")))) + import external_generate + +def refreshInfo(): + '''not used now''' + result = dockletRequest.post('/login/', data) + ok = result and result.get('success', None) + session['username'] = request.form['username'] + session['nickname'] = result['data']['nickname'] + session['description'] = result['data']['description'][0:10] + session['avatar'] = '/static/avatar/'+ result['data']['avatar'] + session['usergroup'] = result['data']['group'] + session['status'] = result['data']['status'] + session['token'] = result['data']['token'] + +class loginView(normalView): + template_path = "login.html" + + @classmethod + def get(self): + if is_authenticated(): + #refreshInfo() + return redirect(request.args.get('next',None) or '/dashboard/') + if (env.getenv('EXTERNAL_LOGIN') == 'True'): + link = external_generate.external_login_link + else: + link = '' + return render_template(self.template_path, link = link) + + @classmethod + def post(self): + if (request.form['username']): + data = {"user": request.form['username'], "key": request.form['password']} + result = dockletRequest.unauthorizedpost('/login/', data) + ok = result and result.get('success', None) + if (ok and (ok == "true")): + # set cookie:docklet-jupyter-cookie for jupyter notebook + resp = make_response(redirect(request.args.get('next',None) or '/dashboard/')) + app_key = os.environ['APP_KEY'] + resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(request.form['username'], app_key)) + # set session for docklet + session['username'] = request.form['username'] + session['nickname'] = result['data']['nickname'] + session['description'] = result['data']['description'] + session['avatar'] = '/static/avatar/'+ result['data']['avatar'] + session['usergroup'] = result['data']['group'] + session['status'] = result['data']['status'] + session['token'] = result['data']['token'] + return resp + else: + return redirect('/login/') + else: + self.error() + +class logoutView(normalView): + + @classmethod + def get(self): + resp = make_response(redirect('/login/')) + session.pop('username', None) + session.pop('nickname', None) + session.pop('description', None) + session.pop('avatar', None) + session.pop('status', None) + session.pop('usergroup', None) + session.pop('token', None) + resp.set_cookie('docklet-jupyter-cookie', '', expires=0) + return resp + + +class external_login_callbackView(normalView): + @classmethod + def get(self): + + form = external_generate.external_auth_generate_request() + result = dockletRequest.unauthorizedpost('/external_login/', form) + ok = result and result.get('success', None) + if (ok and (ok == "true")): + # set cookie:docklet-jupyter-cookie for jupyter notebook + resp = make_response(redirect(request.args.get('next',None) or '/dashboard/')) + app_key = os.environ['APP_KEY'] + resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(result['data']['username'], app_key)) + # set session for docklet + session['username'] = result['data']['username'] + session['nickname'] = result['data']['nickname'] + session['description'] = result['data']['description'] + session['avatar'] = '/static/avatar/'+ result['data']['avatar'] + session['usergroup'] = result['data']['group'] + session['status'] = result['data']['status'] + session['token'] = result['data']['token'] + return resp + else: + return redirect('/login/') + + @classmethod + def post(self): + + form = external_generate.external_auth_generate_request() + result = dockletRequest.unauthorizedpost('/external_login/', form) + ok = result and result.get('success', None) + if (ok and (ok == "true")): + # set cookie:docklet-jupyter-cookie for jupyter notebook + resp = make_response(redirect(request.args.get('next',None) or '/dashboard/')) + app_key = os.environ['APP_KEY'] + resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(result['data']['username'], app_key)) + # set session for docklet + session['username'] = result['data']['username'] + session['nickname'] = result['data']['nickname'] + session['description'] = result['data']['description'] + session['avatar'] = '/static/avatar/'+ result['data']['avatar'] + session['usergroup'] = result['data']['group'] + session['status'] = result['data']['status'] + session['token'] = result['data']['token'] + return resp + else: + return redirect('/login/') + +class external_loginView(normalView): + if (env.getenv('EXTERNAL_LOGIN') == 'True'): + template_path = external_generate.html_path diff --git a/web/webViews/authenticate/register.py b/web/webViews/authenticate/register.py new file mode 100644 index 0000000..1302ea0 --- /dev/null +++ b/web/webViews/authenticate/register.py @@ -0,0 +1,14 @@ +from webViews.view import normalView +from webViews.dockletrequest import dockletRequest +from flask import redirect, request, abort + +class registerView(normalView): + template_path = 'register.html' + + @classmethod + def post(self): + form = dict(request.form) + if (request.form.get('username') == None or request.form.get('password') == None or request.form.get('password') != request.form.get('password2') or request.form.get('email') == None or request.form.get('description') == None): + abort(500) + result = dockletRequest.unauthorizedpost('/register/', form) + return self.render('waitingRegister.html') diff --git a/web/webViews/cluster.py b/web/webViews/cluster.py new file mode 100755 index 0000000..6c130b6 --- /dev/null +++ b/web/webViews/cluster.py @@ -0,0 +1,297 @@ +from flask import session +from webViews.view import normalView +from webViews.dockletrequest import dockletRequest +from webViews.dashboard import * +import time, re + +class addClusterView(normalView): + template_path = "addCluster.html" + + @classmethod + def get(self): + result = dockletRequest.post("/image/list/") + images = result.get("images") + if (result): + return self.render(self.template_path, user = session['username'], images = images) + else: + self.error() + +class createClusterView(normalView): + template_path = "dashboard.html" + error_path = "error.html" + + @classmethod + def post(self): + index1 = self.image.rindex("_") + index2 = self.image[:index1].rindex("_") + data = { + "clustername": self.clustername, + 'imagename': self.image[:index2], + 'imageowner': self.image[index2+1:index1], + 'imagetype': self.image[index1+1:], + } + result = dockletRequest.post("/cluster/create/", data) + if(result.get('success', None) == "true"): + return dashboardView.as_view() + #return self.render(self.template_path, user = session['username']) + else: + return self.render(self.error_path, message = result.get('message')) + +class descriptionImageView(normalView): + template_path = "image_description.html" + + @classmethod + def get(self): + index1 = self.image.rindex("_") + index2 = self.image[:index1].rindex("_") + data = { + "imagename": self.image[:index2], + "imageowner": self.image[index2+1:index1], + "imagetype": self.image[index1+1:] + } + result = dockletRequest.post("/image/description/", data) + if(result): + description = result.get("message") + return self.render(self.template_path, description = description) + else: + self.error() + +class scaleoutView(normalView): + error_path = "error.html" + + @classmethod + def post(self): + index1 = self.image.rindex("_") + index2 = self.image[:index1].rindex("_") + data = { + "clustername": self.clustername, + 'imagename': self.image[:index2], + 'imageowner': self.image[index2+1:index1], + 'imagetype': self.image[index1+1:] + } + result = dockletRequest.post("/cluster/scaleout/", data) + if(result.get('success', None) == "true"): + return configView.as_view() + else: + return self.render(self.error_path, message = result.get('message')) + +class scaleinView(normalView): + @classmethod + def get(self): + data = { + "clustername": self.clustername, + "containername":self.containername + } + result = dockletRequest.post("/cluster/scalein/", data) + if(result): + return configView.as_view() + else: + self.error() + +class listClusterView(normalView): + template_path = "listCluster.html" + + @classmethod + def get(self): + result = dockletRequest.post("/cluster/list/") + clusters = result.get("clusters") + if(result): + return self.render(self.template_path, user = session['username'], clusters = clusters) + else: + self.error() + +class startClusterView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "clustername": self.clustername + } + result = dockletRequest.post("/cluster/start/", data) + if(result): + return dashboardView.as_view() + else: + return self.error() + +class stopClusterView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "clustername": self.clustername + } + result = dockletRequest.post("/cluster/stop/", data) + if(result): + return dashboardView.as_view() + else: + return self.error() + +class flushClusterView(normalView): + success_path = "opsuccess.html" + failed_path = "opfailed.html" + + @classmethod + def get(self): + data = { + "clustername": self.clustername, + "from_lxc": self.containername + } + result = dockletRequest.post("/cluster/flush/", data) + + if(result): + if result.get('success') == "true": + return self.render(self.success_path, user = session['username']) + else: + return self.render(self.failed_path, user = session['username']) + else: + self.error() + +class deleteClusterView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "clustername": self.clustername + } + result = dockletRequest.post("/cluster/delete/", data) + if(result): + return dashboardView.as_view() + else: + return self.error() + +class detailClusterView(normalView): + template_path = "listcontainer.html" + + @classmethod + def get(self): + data = { + "clustername": self.clustername + } + result = dockletRequest.post("/cluster/info/", data) + if(result): + message = result.get('message') + containers = message['containers'] + status = message['status'] + return self.render(self.template_path, containers = containers, user = session['username'], clustername = self.clustername, status = status) + else: + self.error() + +class saveImageView(normalView): + template_path = "saveconfirm.html" + success_path = "opsuccess.html" + + @classmethod + def post(self): + data = { + "clustername": self.clustername, + "image": self.imagename, + "containername": self.containername, + "description": self.description, + "isforce": self.isforce + } + result = dockletRequest.post("/cluster/save/", data) + if(result): + if result.get('success') == 'true': + #return self.render(self.success_path, user = session['username']) + return configView.as_view() + #res = detailClusterView() + #res.clustername = self.clustername + #return res.as_view() + else: + return self.render(self.template_path, containername = self.containername, clustername = self.clustername, image = self.imagename, user = session['username'], description = self.description) + else: + self.error() + +class shareImageView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "image": self.image + } + result = dockletRequest.post("/image/share/", data) + if(result): + return configView.as_view() + else: + self.error() + +class unshareImageView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "image": self.image + } + result = dockletRequest.post("/image/unshare/", data) + if(result): + return configView.as_view() + else: + self.error() + +class deleteImageView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + data = { + "image": self.image + } + result = dockletRequest.post("/image/delete/", data) + if(result): + return configView.as_view() + else: + self.error() + +class addproxyView(normalView): + + @classmethod + def post(self): + data = { + "clustername": self.clustername, + "ip": self.ip, + "port": self.port + } + result = dockletRequest.post("/addproxy/", data) + if(result): + return configView.as_view() + else: + self.error() + +class deleteproxyView(normalView): + + @classmethod + def get(self): + data = { + "clustername":self.clustername + } + result = dockletRequest.post("/deleteproxy/", data) + if(result): + return configView.as_view() + else: + self.error() + + @classmethod + def post(self): + return self.get() + +class configView(normalView): + @classmethod + def get(self): + images = dockletRequest.post('/image/list/').get('images') + clusters = dockletRequest.post("/cluster/list/").get("clusters") + clusters_info = {} + data={} + for cluster in clusters: + data["clustername"] = cluster + result = dockletRequest.post("/cluster/info/",data).get("message") + clusters_info[cluster] = result + return self.render("config.html", images = images, clusters = clusters_info, mysession=dict(session)) + + @classmethod + def post(self): + return self.get() diff --git a/web/webViews/cookie_tool.py b/web/webViews/cookie_tool.py new file mode 100755 index 0000000..83a3326 --- /dev/null +++ b/web/webViews/cookie_tool.py @@ -0,0 +1,52 @@ +#!/usr/bin/python3 + +import json, hashlib, base64, time +import sys +from webViews.log import logger + +# generate cookie : +# name = 'leebaok' +# | +# { "name":"leebaok", "login-time":time} Secure-Key +# | | +# | json.dumps | +# | | +# '{ "name":"leebaok", "login-time":time}' ______________| +# | | concat +# | encode('ascii') -> base64 | encode('ascii') -> md5().hexdigest() +# | str(*, encoding='utf-8') | +# | | +# < XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX >.< XXXXXXXXXXXXXXXXXXXX > +# + +def generate_cookie(name, securekey): + #print (">> generate cookie for %s" % name) + content = { 'name':name, 'login-time': time.asctime() } + text = json.dumps(content) + part1 = base64.b64encode(text.encode('ascii')) + part2 = hashlib.md5( (text+securekey).encode('ascii') ).hexdigest() + # part1 is binary(ascii) and part2 is str(utf-8) + cookie = str(part1, encoding='utf-8') +"."+ part2 + #print ("cookie : %s" % cookie) + return cookie + +def parse_cookie(cookie, securekey): + logger.info (">> parse cookie : %s" % cookie) + parts = cookie.split('.') + part1 = parts[0] + part2 = '' if len(parts) < 2 else parts[1] + try: + text = str(base64.b64decode(part1.encode('ascii')), encoding='utf-8') + except: + logger.info ("decode cookie failed") + return None + logger.info ("cookie content : %s" % text) + thatpart2 = hashlib.md5((text+securekey).encode('ascii')).hexdigest() + logger.info ("hash from part1 : %s" % thatpart2) + logger.info ("hash from part2 : %s" % part2) + if part2 == thatpart2: + result = json.loads(text)['name'] + else: + result = None + logger.info ("parse from cookie : %s" % result) + return result diff --git a/web/webViews/dashboard.py b/web/webViews/dashboard.py new file mode 100644 index 0000000..cf07cb5 --- /dev/null +++ b/web/webViews/dashboard.py @@ -0,0 +1,53 @@ +from flask import session,render_template +from webViews.view import normalView +from webViews.dockletrequest import dockletRequest + + +class dashboardView(normalView): + template_path = "dashboard.html" + + @classmethod + def get(self): + result = dockletRequest.post('/cluster/list/') + images = dockletRequest.post('/image/list/').get("images") + ok = result and result.get('clusters') + clusters = result.get("clusters") + if (result): + full_clusters = [] + data={} + for cluster in clusters: + data["clustername"] = cluster + single_cluster = {} + single_cluster['name'] = cluster + message = dockletRequest.post("/cluster/info/", data) + if(message): + message = message.get("message") + single_cluster['status'] = message['status'] + single_cluster['id'] = message['clusterid'] + full_clusters.append(single_cluster) + else: + self.error() + return self.render(self.template_path, ok = ok, clusters = full_clusters, images = images) + else: + self.error() + + @classmethod + def post(self): + return self.get() + +class dashboard_guestView(normalView): + template_path = "dashboard_guest.html" + + @classmethod + def get(self): + mysession = {} + mysession['avatar'] = "/static/avatar/default.png" + mysession['nickname'] = "guest" + mysession['description'] = "you are a guest" + mysession['status'] = "guest" + mysession['usergroup'] = "normal" + return render_template(self.template_path, mysession = mysession) + + @classmethod + def post(self): + return self.get() diff --git a/web/webViews/dockletrequest.py b/web/webViews/dockletrequest.py new file mode 100644 index 0000000..c6e1267 --- /dev/null +++ b/web/webViews/dockletrequest.py @@ -0,0 +1,32 @@ +import requests +import json +from flask import abort, session +from webViews.log import logger + + +endpoint = "http://0.0.0.0:9000" + +class dockletRequest(): + + @classmethod + def post(self, url = '/', data = {}): + #try: + data = dict(data) + data['token'] = session['token'] + logger.info ("Docklet Request: user = %s data = %s, url = %s"%(session['username'], data, url)) + + result = requests.post(endpoint + url, data = data).json() + if (result.get('success', None) == "false" and (result.get('reason', None) == "Unauthorized Action" or result.get('Unauthorized', None) == 'True')): + abort(401) + logger.info ("Docklet Response: user = %s result = %s, url = %s"%(session['username'], result, url)) + return result + #except: + #abort(500) + + @classmethod + def unauthorizedpost(self, url = '/', data = None): + data = dict(data) + logger.info("Docklet Unauthorized Request: data = %s, url = %s" % (data, url)) + result = requests.post(endpoint + url, data = data).json() + logger.info("Docklet Unauthorized Response: result = %s, url = %s"%(result, url)) + return result diff --git a/web/webViews/log.py b/web/webViews/log.py new file mode 100644 index 0000000..4459491 --- /dev/null +++ b/web/webViews/log.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python + +import logging +import logging.handlers +import argparse +import sys +import time # this is only being used as part of the example +import os + +import os, sys, inspect +this_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) +src_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,"../..", "src"))) +if src_folder not in sys.path: + sys.path.insert(0, src_folder) +import env + +# logger should only be imported after initlogging has been called +logger = None + +def initlogging(name='docklet'): + # Deafults + global logger + + homepath = env.getenv('FS_PREFIX') + LOG_FILENAME = homepath + '/local/log/' + name + '.log' + + LOG_LEVEL = env.getenv('WEB_LOG_LEVEL') + if LOG_LEVEL == "DEBUG": + LOG_LEVEL = logging.DEBUG + elif LOG_LEVEL == "INFO": + LOG_LEVEL = logging.INFO + elif LOG_LEVEL == "WARNING": + LOG_LEVEL = logging.WARNING + elif LOG_LEVEL == "ERROR": + LOG_LEVEL = logging.ERROR + elif LOG_LEVEL == "CRITICAL": + LOG_LEVEL = logging.CRITIAL + else: + LOG_LEVEL = logging.DEBUG + + logger = logging.getLogger(name) + # Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data + # Give the logger a unique name (good practice) + # Set the log level to LOG_LEVEL + logger.setLevel(LOG_LEVEL) + # Make a handler that writes to a file, making a new file at midnight and keeping 3 backups + handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, + when="midnight", backupCount=0) + # Format each log message like this + formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(module)s[%(lineno)d] %(message)s') + # Attach the formatter to the handler + handler.setFormatter(formatter) + # Attach the handler to the logger + logger.addHandler(handler) + + # Replace stdout with logging to file at INFO level + sys.stdout = RedirectLogger(logger, logging.INFO) + # Replace stderr with logging to file at ERROR level + sys.stderr = RedirectLogger(logger, logging.ERROR) + + # Make a class we can use to capture stdout and sterr in the log +class RedirectLogger(object): + def __init__(self, logger, level): + """Needs a logger and a logger level.""" + self.logger = logger + self.level = level + + def write(self, message): + # Only log if there is a message (not just a new line) + if message.rstrip() != "": + self.logger.log(self.level, message.rstrip()) + + def flush(self): + for handler in self.logger.handlers: + handler.flush() diff --git a/web/webViews/monitor.py b/web/webViews/monitor.py new file mode 100755 index 0000000..e25eb42 --- /dev/null +++ b/web/webViews/monitor.py @@ -0,0 +1,113 @@ +from flask import session +from webViews.view import normalView +from webViews.dockletrequest import dockletRequest + + +class statusView(normalView): + template_path = "monitor/status.html" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/cluster/list/', data) + clusters = result.get('clusters') + if (result): + containers = {} + for cluster in clusters: + data["clustername"] = cluster + message = dockletRequest.post('/cluster/info/', data) + if (message): + message = message.get('message') + else: + self.error() + containers[cluster] = message + return self.render(self.template_path, clusters = clusters, containers = containers, user = session['username']) + else: + self.error() + +class statusRealtimeView(normalView): + template_path = "monitor/statusRealtime.html" + node_name = "" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/monitor/vnodes/%s/basic_info'%(self.node_name), data) + basic_info = result.get('monitor').get('basic_info') + return self.render(self.template_path, node_name = self.node_name, user = session['username'], container = basic_info) + +class hostsRealtimeView(normalView): + template_path = "monitor/hostsRealtime.html" + com_ip = "" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/monitor/hosts/%s/cpuconfig'%(self.com_ip), data) + proc = result.get('monitor').get('cpuconfig') + result = dockletRequest.post('/monitor/hosts/%s/osinfo'%(self.com_ip), data) + osinfo = result.get('monitor').get('osinfo') + result = dockletRequest.post('/monitor/hosts/%s/diskinfo'%(self.com_ip), data) + diskinfos = result.get('monitor').get('diskinfo') + + return self.render(self.template_path, com_ip = self.com_ip, user = session['username'],processors = proc, OSinfo = osinfo, diskinfos = diskinfos) + +class hostsConAllView(normalView): + template_path = "monitor/hostsConAll.html" + com_ip = "" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/monitor/hosts/%s/containerslist'%(self.com_ip), data) + containers = result.get('monitor').get('containerslist') + containerslist = [] + for container in containers: + result = dockletRequest.post('/monitor/vnodes/%s/basic_info'%(container), data) + basic_info = result.get('monitor').get('basic_info') + containerslist.append(basic_info) + return self.render(self.template_path, containerslist = containerslist, com_ip = self.com_ip, user = session['username']) + +class hostsView(normalView): + template_path = "monitor/hosts.html" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/monitor/listphynodes', data) + iplist = result.get('monitor').get('allnodes') + machines = [] + for ip in iplist: + containers = {} + result = dockletRequest.post('/monitor/hosts/%s/containers'%(ip), data) + containers = result.get('monitor').get('containers') + result = dockletRequest.post('/monitor/hosts/%s/status'%(ip), data) + status = result.get('monitor').get('status') + machines.append({'ip':ip,'containers':containers, 'status':status}) + #print(machines) + return self.render(self.template_path, machines = machines, user = session['username']) + +class monitorUserAllView(normalView): + template_path = "monitor/monitorUserAll.html" + + @classmethod + def get(self): + data = { + "user": session['username'], + } + result = dockletRequest.post('/monitor/listphynodes', data) + userslist = [{'name':'root'},{'name':'libao'}] + for user in userslist: + result = dockletRequest.post('/monitor/user/%s/clustercnt'%(user['name']), data) + user['clustercnt'] = result.get('monitor').get('clustercnt') + return self.render(self.template_path, userslist = userslist, user = session['username']) diff --git a/web/webViews/user/grouplist.py b/web/webViews/user/grouplist.py new file mode 100644 index 0000000..d096238 --- /dev/null +++ b/web/webViews/user/grouplist.py @@ -0,0 +1,23 @@ +from flask import redirect, request +from webViews.dockletrequest import dockletRequest +from webViews.view import normalView +import json + +class grouplistView(normalView): + template_path = "user/grouplist.html" + +class groupdetailView(normalView): + @classmethod + def post(self): + return json.dumps(dockletRequest.post('/user/groupList/')) + +class groupqueryView(normalView): + @classmethod + def post(self): + return json.dumps(dockletRequest.post('/user/groupQuery/', request.form)) + +class groupmodifyView(normalView): + @classmethod + def post(self): + result = json.dumps(dockletRequest.post('/user/groupModify/', request.form)) + return redirect('/admin/') diff --git a/web/webViews/user/userActivate.py b/web/webViews/user/userActivate.py new file mode 100644 index 0000000..c8fe2b1 --- /dev/null +++ b/web/webViews/user/userActivate.py @@ -0,0 +1,20 @@ +from flask import render_template, redirect, request +from webViews.dockletrequest import dockletRequest +from webViews.view import normalView + + +class userActivateView(normalView): + template_path = 'user/activate.html' + + @classmethod + def get(self): + userinfo = dockletRequest.post('/user/selfQuery/') + userinfo = userinfo["data"] + if (userinfo["description"] == ''): + userinfo["description"] = "Describe why you want to use Docklet" + return self.render(self.template_path, info = userinfo) + + @classmethod + def post(self): + dockletRequest.post('/register', request.form) + return redirect('/logout/') diff --git a/web/webViews/user/userinfo.py b/web/webViews/user/userinfo.py new file mode 100644 index 0000000..5f6b792 --- /dev/null +++ b/web/webViews/user/userinfo.py @@ -0,0 +1,18 @@ +from flask import redirect, request +from webViews.dockletrequest import dockletRequest +from webViews.view import normalView +import json + +class userinfoView(normalView): + template_path = "user/info.html" + + @classmethod + def get(self): + userinfo = dockletRequest.post('/user/selfQuery/') + userinfo = userinfo["data"] + return self.render(self.template_path, info = userinfo) + + @classmethod + def post(self): + result = json.dumps(dockletRequest.post('/user/selfModify/', request.form)) + return result diff --git a/web/webViews/user/userlist.py b/web/webViews/user/userlist.py new file mode 100644 index 0000000..c3cc62a --- /dev/null +++ b/web/webViews/user/userlist.py @@ -0,0 +1,56 @@ +from flask import render_template, redirect, request +from webViews.dockletrequest import dockletRequest +from webViews.view import normalView +import json + +class userlistView(normalView): + template_path = "user_list.html" + + @classmethod + def get(self): + groups = dockletRequest.post('/user/groupNameList/')["groups"] + return self.render(self.template_path, groups = groups) + + @classmethod + def post(self): + return json.dumps(dockletRequest.post('/user/data/')) + + +class useraddView(normalView): + @classmethod + def post(self): + dockletRequest.post('/user/add', request.form) + return redirect('/user/list/') + +class userdataView(normalView): + @classmethod + def get(self): + return json.dumps(dockletRequest.post('/user/data', request.form)) + + @classmethod + def post(self): + return json.dumps(dockletRequest.post('/user/data', request.form)) + +class userqueryView(normalView): + @classmethod + def get(self): + return json.dumps(dockletRequest.post('/user/query', request.form)) + + @classmethod + def post(self): + return json.dumps(dockletRequest.post('/user/query', request.form)) + +class usermodifyView(normalView): + @classmethod + def post(self): + try: + dockletRequest.post('/user/modify', request.form) + except: + return self.render('user/mailservererror.html') + return redirect('/user/list/') + +class groupaddView(normalView): + @classmethod + def post(self): + dockletRequest.post('/user/groupadd', request.form) + return redirect('/admin/') diff --git a/web/webViews/view.py b/web/webViews/view.py new file mode 100644 index 0000000..f1b4497 --- /dev/null +++ b/web/webViews/view.py @@ -0,0 +1,39 @@ +from flask import render_template, request, abort, session + +import os, inspect +this_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0])) + +version_file = open(this_folder + '/../../VERSION') +version = version_file.read() +version_file.close() + +class normalView(): + template_path = "dashboard.html" + + @classmethod + def get(self): + return self.render(self.template_path) + + @classmethod + def post(self): + return self.render(self.template_path) + + @classmethod + def error(self): + abort(404) + + @classmethod + def as_view(self): + if request.method == 'GET': + return self.get() + elif request.method == 'POST': + return self.post() + else: + return self.error() + + @classmethod + def render(self, *args, **kwargs): + self.mysession = dict(session) + kwargs['mysession'] = self.mysession + kwargs['version'] = version + return render_template(*args, **kwargs)