[
  {
    "path": ".gitignore",
    "content": "__pycache__\n*.pyc\n*.swp\n__temp\n*~\n.DS_Store\ndocklet.conf\nhome.html\nsrc/utils/migrations/\ncontainer.conf\ncontainer.batch.conf\n"
  },
  {
    "path": "CHANGES",
    "content": "v0.4.0,  May 26, 2019\n--------------------\n\n**Bug Fix**\n  * Fix a bug of update base image.\n  * Fix a bug of port control & a bug of update_v0.3.2.py.\n  * Add locks to solve synchronization problems.\n  * Fix a type error in web/web.py.\n  * Fix a bug that net stats can't be shown.\n\n**Improvement**\n  * [#298 #299 #300 ] Support batch computing.\n  * Add information of login to user log and database.\n  * Prevent users that is not activated from applying for beans.\n  * Aggregate api of monitor at the backend and aggregate http request on status realtime pages for monitor information.\n  * Support user to report a bug in dashboard.\n  * Display image size when creating vcluster.\n  * Security enhancement: forbid double slashes url, add header into nginx to defend clickjacking, add CsrfProtect, forbid methods except for GET and POST in nginx and support https...\n  * Add LoginFailMsg into model & Ban user if he input wrong password for many times.\n  * Add UDP4 mapping for iptables.\n  * Support migrating containers.\n  * Support releasing vcluster when it is stopped for too long automatically.\n\nv0.3.2,  Dec 11, 2017\n--------------------\n\n**Bug Fix**\n  * Fix the problem that some monitoring data are used before initialnizing.\n  * Add some error message when start service failed.\n  * Add npm registry.\n\n**Improvement**\n  * [#277] Support egress and ingress qos rate limiting.\n  * [#277] Support network and ports mappings billings.\n  * Support network monitoring.\n  * Limit the number of users' vnodes by ip addresses.\n  * Add billing detail and billing history detail\n  * Replace lxc-info with lxc.Container.get_cgroup_item()\n\nv0.3.0,  Sep 29, 2017\n--------------------\n\n**Bug Fix**\n  * [#180] generated_password file no exist after master init\n  * Release ip when create container failed.\n\n**Improvement**\n  * [#16] display file size, modification time in jupyter notebook \n  * [#86] time display in UserList\n  * [#87] add a new panel to approve or decline user activation requests\n  * [#121] Autofilling may lead to a bug that makes local users cannot login\n  * [#178] record and display history of all containers\n  * [#210] rename Dashboard to Workspace\n  * [#212] add docklet hyperlink in web portal \n  * Separate user module from master.\n  * Support multiple masters run in the same time, and the user can choose which to use in webpage.\n  * Support distributed gateway, if enable, worker will setup its own gateway.\n  * Support user gateway.\n\nv0.2.8,  Jul 28, 2016\n--------------------\n\n**Bug Fix**\n  * [#119] version display error\n\n**Improvement**\n  * [#52] give user a total quota, let themselves decide how to use quota\n  * [#72] recording the user's historical resource usage\n  * [#85] Making workers's state consistent with master\n  * [#88] setting config file in admin panel\n  * [#96] Web notifications\n  * [#113] Recovery : after poweroff, just recover container, not recover service\n\nv0.2.7,  May 17, 2016\n--------------------\n\n**Bug Fix**\n  * [#9] updating user profile taking effects immediately\n  * [#12] logging user's activity\n  * [#14] Can't stop vcluster by dashboard page\n  * [#18] subprocess call should check return status\n  * [#19] lxc config string in config file is limited in 16 bytes\n  * [#25] bug of external login\n  * [#30] support lxc.custom.conf in appending\n  * [#35] nfs mountpoint bug in imagemgr.py\n  * [#49] Fail to create container\n  * [#57] status page of normal user failed\n  * [#68] Not Found error when just click \"Sign in\" Button\n  * [#76] unable to show and edit user table in smartphone\n\n**Improvement**\n  * [#7] enhance quota management\n  * [#8] independent starting of master and workers\n  * [#20] check typing and input on web pages and web server\n  * [#23] add LXCFS for container\n  * [#41] move system data to global/sys\n  * [#42] check IP and network pool when releasing IPs\n  * [#48] token expires after some time\n  * [#54] display container owner\n  * [#61] rewrite httprest.py using flask routing\n\n**Notes**\n  * If you upgrade from former version, please run tools/upgrade.py first.\n\nv0.2.6,  Mar 31, 2016\n--------------------\n\nAn initial release on github.com\n\n* Using the open source AdminLTE theme\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2016, Peking University (PKU).\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n1. Redistributions of source code must retain the above copyright \n   notice, this list of conditions and the following disclaimer.\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n3. Neither the name of the PKU nor the names of its contributors\n   may be used to endorse or promote products derived from this software\n   without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\nOR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\nHOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\nLIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\nOUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGE.INCLUDING NEGLIGENCE OR OTHERWISE\n"
  },
  {
    "path": "README.md",
    "content": "# Docklet\n\nhttps://unias.github.io/docklet\n\n## Intro\n\nDocklet is an operating system for virtual private cloud. Its goal is\nto help a user group effectively share cluster resources in physical \ndatacenter or in the cloud. In Docklet, the shared resources are organized\nand managed as a virtual private cloud among the user group. Every user \nhas their own private **virtual cluster (vcluster)**, which consists of \na number of virtual Linux container nodes distributed over the physical \ncluster. Each vcluster is separated from others and can be operated like \na real physical cluster. Therefore, most applications, especially those \nrequiring a cluster of resources can run in vcluster seamlessly. \n\nUsers manage and use their vcluster resources all through web. The supported\nresources include CPUs, GPUs, shared storage, etc. The only client\ntool needed is a modern web browser supporting HTML5, like Safari,\nFirefox, or Chrome.  The integrated *jupyter notebook* provides a web\n**Workspace**. Users can code, debug, test, and run their programs, \neven visualize the outputs online. Serverless computing and batch \nprocessing is supported.\n\nDocklet creates virtual nodes from a base image. Admins can\npre-install development tools and frameworks according to their\ninterests. The users are also free to install their specific software\nin their vcluster.\n\nDocklet only need **one** public IP address. The vclusters are\nconfigured to use private IP address range, e.g., 172.16.0.0/16,\n192.168.0.0/16, 10.0.0.0/8. A proxy is setup to help\nusers visit their vclusters behind the firewall/gateway.\n\n## Architecture\n\nThe Docklet system runtime consists of four main components:\n\n- distributed file system server\n- etcd server\n- docklet supermaster, master\n- docklet worker\n\n![](./docklet-arch.png)\n\nFor detailed information about configurations, please see [Config](#config).\n\n## Install\n\nCurrently the Docklet system is recommend to run in Ubuntu 15.10+.\n\nEnsure that python3.5 is the default python3 version.\n\nClone Docklet from github\n\n```\ngit clone  https://github.com/unias/docklet.git\n```\n\nRun **prepare.sh** from console to install depended packages and\ngenerate necessary configurations.\n\nA *root* users will be created for managing the Docklet system. The\npassword is recorded in `FS_PREFIX/local/generated_password.txt` .\n\n## Config ##\n\nThe main configuration file of docklet is conf/docklet.conf. Most\ndefault setting works for a single host environment.\n\nFirst copy docklet.conf.template to get docklet.conf.\n\nPay attention to the following settings:\n\n- NETWORK_DEVICE : the network interface to use.\n- ETCD : the etcd server address. For distributed multi hosts\n  environment, it should be one of the ETCD public server address.\n  For single host environment, the default value should be OK.\n- STORAGE : using disk or file to storage persistent data, for\n  single host, file is convenient.\n- FS_PREFIX: the working dir of docklet runtime. default is\n  /opt/docklet.\n- CLUSTER_NET: the vcluster network ip address range, default is\n  172.16.0.1/16. This network range should all be allocated to  and\n  managed by docklet.\n- PROXY_PORT : the listening port of configurable-http-proxy. It proxy\n  connections from external public network to internal private\n  container networks.\n- PORTAL_URL : the portal of the system. Users access the system\n  by visiting this address. If the system is behind a firewall, then\n  a reverse proxy should be setup. Default is MASTER_IP:NGINX_PORT.\n- NGINX_PORT : the access port of the public portal. User use this\n  port to visit docklet system.\n- DISTRIBUTED_GATEWAY : whether the users' gateways are distributed\n  or not. Both master and worker must be set by same value.\n- PUBLIC_IP : public ip of this machine. If DISTRIBUTED_GATEWAY is True,\n  users' gateways can be setup on this machine. Users can visit this\n  machine by the public ip. default: IP of NETWORK_DEVICE.\n- USER_IP : the ip of user server. default : localhost\n- MASTER_IPS : tell the web server the ips of all the cluster master.\n- AUTH_KEY: the key to request users server from master, or to request\n  master from users server. Please set the same value on each machine.\n  Please don't use the default value.\n\n## Start ##\n\n### distributed file system ###\n\nFor multi hosts distributed environment, a distributed file system is\nneeded to store global data. Currently, glusterfs has been tested.\nLets presume the file system server export filesystem as nfs\n**fileserver:/pub** :\n\nIn each physical host to run docklet, mount **fileserver:/pub** to\n**FS_PEFIX/global** .\n\nFor single host environment, nothing to do.\n\n### etcd ###\n\nFor single host environment, start **tools/etcd-one-node.sh** . Some recent\nUbuntu releases have included **etcd** in the repository, just `apt-get\ninstall etcd`, and it need not to start etcd manually. For others, you\nshould install etcd manually.\n\nFor multi hosts distributed environment, **must** start\n**tools/etcd-multi-nodes.sh** in each etcd server hosts. This scripts\nrequires users providing the etcd server address as parameters.\n\n### supermaster ###\n\nSupermaster is a server consist of web server, user server and a master server instance.\n\nIf it is the first time you start docklet, run `bin/docklet-supermaster init`\nto init and start a docklet master, web server and user server. Otherwise, run `bin/docklet-supermaster start`.\nWhen you start a supermaster, you don't need to start an extra master in the same cluster.\n\n### master ###\n\nA master manages all the workers in one data center. Docklet can manage\nseveral data centers, each data center has one master server. But\na docklet system will only have one supermaster.\n\nFirst, select a server with 2 network interface card, one having a\npublic IP address/url, e.g., docklet.info; the other having a private IP\naddress, e.g., 172.16.0.1. This server will be the master.\n\nIf it is the first time you start docklet, run `bin/docklet-master init`\nto init and start docklet master. Otherwise, run  `bin/docklet-master start`,\nwhich will start master in recovery mode in background using\nconf/docklet.conf. (Note: if docklet will run in the distributed gateway mode\nand recovery mode, please start the workers first.)\n\nPlease fill the USER_IP and USER_PORT in conf/docklet.conf, it is the ip and port of user server.\nBy default, it is `localhost` and `9100`\n\nYou can check the daemon status by running `bin/docklet-master status`\n\nThe master logs are in **FS_PREFIX/local/log/docklet-master.log** and\n**docklet-web.log**.\n\n### worker ###\n\nWorker needs a basefs image to create containers.\n\nYou can create such an image with `lxc-create -n test -t download`,\nthen copy the rootfs to **FS_PREFIX/local**, and rename `rootfs`\nto `basefs`.\n\nNote the `jupyerhub` package must be installed for this image.  And the\nstart script `tools/start_jupyter.sh` should be placed at\n`basefs/home/jupyter`.\n\nYou can check and run `tools/update-basefs.sh` to update basefs.\n\nRun `bin/docklet-worker start`, will start worker in background.\n\nYou can check the daemon status by running `bin/docklet-worker status`.\n\nThe log is in **FS_PREFIX/local/log/docklet-worker.log**.\n\nCurrently, the worker must be run after the master has been started.\n\n## Usage ##\n\nOpen a browser, visiting the address specified by PORTAL_URL ,\ne.g., ` http://docklet.info/ `\n\nThat is it.\n\n# Contribute #\n\nContributions are welcome. Please check [devguide](doc/devguide/devguide.md)\n"
  },
  {
    "path": "VERSION",
    "content": "0.4.0\n"
  },
  {
    "path": "bin/docklet-master",
    "content": "#!/bin/sh\n\n[ $(id -u) != '0' ] && echo \"root is needed\" && exit 1\n\n# get some path of docklet\nbindir=${0%/*}\n# $bindir maybe like /opt/docklet/src/../sbin\n# use command below to make $bindir in normal absolute path\nDOCKLET_BIN=$(cd $bindir; pwd)\nDOCKLET_HOME=${DOCKLET_BIN%/*}\nDOCKLET_CONF=$DOCKLET_HOME/conf\nLXC_SCRIPT=$DOCKLET_CONF/lxc-script\nDOCKLET_SRC=$DOCKLET_HOME/src\nDOCKLET_LIB=$DOCKLET_SRC\nDOCKLET_WEB=$DOCKLET_HOME/web\nDOCKLET_USER=$DOCKLET_HOME/user\n\n# default working directory, default to /opt/docklet\nFS_PREFIX=/opt/docklet\n\n#network interface , default is eth0\nNETWORK_DEVICE=eth0\n#etcd server address, default is localhost:2379\nETCD=localhost:2379\n#unique cluster_name, default is docklet-vc\nCLUSTER_NAME=docklet-vc\n#web port, default is 8888\nWEB_PORT=8888\nUSER_PORT=9100\n#cluster net, default is 172.16.0.1/16\nCLUSTER_NET=\"172.16.0.1/16\"\n# ip addresses range of containers for batch job, default is 10.16.0.0/16\nBATCH_NET=\"10.16.0.0/16\"\n#configurable-http-proxy public port, default is 8000\nPROXY_PORT=8000\n#configurable-http-proxy api port, default is 8001\nPROXY_API_PORT=8001\nDISTRIBUTED_GATEWAY=False\n\n. $DOCKLET_CONF/docklet.conf\n\nexport FS_PREFIX\n\nRUN_DIR=$FS_PREFIX/local/run\nLOG_DIR=$FS_PREFIX/local/log\n\n# This next line determines what user the script runs as.\nDAEMON_USER=root\n\n# settings for docklet master\nDAEMON_MASTER=$DOCKLET_LIB/master/httprest.py\nDAEMON_NAME_MASTER=docklet-master\nDAEMON_OPTS_MASTER=\n# The process ID of the script when it runs is stored here:\nPIDFILE_MASTER=$RUN_DIR/$DAEMON_NAME_MASTER.pid\n\n# settings for docklet web\nDAEMON_WEB=$DOCKLET_WEB/web.py\nDAEMON_NAME_WEB=docklet-web\nPIDFILE_WEB=$RUN_DIR/docklet-web.pid\nDAEMON_OPTS_WEB=\n\n# settings for docklet proxy, which is required for web access\nDAEMON_PROXY=`which configurable-http-proxy`\nDAEMON_NAME_PROXY=docklet-proxy\nPIDFILE_PROXY=$RUN_DIR/proxy.pid\nDAEMON_OPTS_PROXY=\n# settings for docklet user\nDAEMON_USER_MODULE=$DOCKLET_USER/user.py\nDAEMON_NAME_USER=docklet-user\nPIDFILE_USER=$RUN_DIR/docklet-user.pid\nDAEMON_OPTS_USER=\n\nRUNNING_CONFIG=$FS_PREFIX/local/docklet-running.conf\nexport CONFIG=$RUNNING_CONFIG\n\n. /lib/lsb/init-functions\n\n###########\n\npre_start_master () {\n    log_daemon_msg \"Starting $DAEMON_NAME_MASTER in $FS_PREFIX\"\n\n    [ ! -d $FS_PREFIX/global ] && mkdir -p $FS_PREFIX/global\n    [ ! -d $FS_PREFIX/local ] && mkdir -p $FS_PREFIX/local\n    [ ! -d $FS_PREFIX/global/users ] && mkdir -p $FS_PREFIX/global/users\n    [ ! -d $FS_PREFIX/global/sys ] && mkdir -p $FS_PREFIX/global/sys\n    [ ! -d $FS_PREFIX/global/images/private ] && mkdir -p $FS_PREFIX/global/images/private\n    [ ! -d $FS_PREFIX/global/images/public ] && mkdir -p $FS_PREFIX/global/images/public\n    [ ! -d $FS_PREFIX/local/volume ] && mkdir -p $FS_PREFIX/local/volume\n    [ ! -d $FS_PREFIX/local/temp ] && mkdir -p $FS_PREFIX/local/temp\n    [ ! -d $FS_PREFIX/local/run ] && mkdir -p $FS_PREFIX/local/run\n    [ ! -d $FS_PREFIX/local/log ] && mkdir -p $FS_PREFIX/local/log\n\n    grep -P \"^[\\s]*[a-zA-Z]\" $DOCKLET_CONF/docklet.conf > $RUNNING_CONFIG\n\n    echo \"DOCKLET_HOME=$DOCKLET_HOME\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_BIN=$DOCKLET_BIN\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_CONF=$DOCKLET_CONF\" >> $RUNNING_CONFIG\n    echo \"LXC_SCRIPT=$LXC_SCRIPT\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_SRC=$DOCKLET_SRC\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_LIB=$DOCKLET_LIB\" >> $RUNNING_CONFIG\n\n\n    # iptables for NAT network for containers to access web\n    iptables -t nat -F\n    iptables -t nat -A POSTROUTING -s $CLUSTER_NET -j MASQUERADE\n    iptables -t nat -A POSTROUTING -s $BATCH_NET -j MASQUERADE\n\n}\n\ndo_start_master () {\n\n    DAEMON_OPTS_MASTER=$1\n\n\t# MODE : start mode\n\t#    new : clean old data in etcd, global directory and start a new cluster\n\t#    recovery : start cluster and recover status from etcd and global directory\n\t# Default is \"recovery\"\n\n    start-stop-daemon --start --oknodo --background --pidfile $PIDFILE_MASTER --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_MASTER -- $DAEMON_OPTS_MASTER\n    log_end_msg $?\n}\n\npre_start_web () {\n    log_daemon_msg \"Starting $DAEMON_NAME_WEB in $FS_PREFIX\"\n\n    webip=$(ip addr show $NETWORK_DEVICE | grep -oE \"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+/[0-9]+\")\n\n    [ $? != \"0\" ] && echo \"wrong NETWORK_DEVICE $NETWORK_DEVICE\" && exit 1\n\n    webip=${webip%/*}\n\n    AUTH_COOKIE_URL=http://$webip:$WEB_PORT/jupyter\n    #echo \"set AUTH_COOKIE_URL:$AUTH_COOKIE_URL in etcd with key:$CLUSTER_NAME/web/authurl\"\n    curl -XPUT http://$ETCD/v2/keys/$CLUSTER_NAME/web/authurl -d value=\"$AUTH_COOKIE_URL\" > /dev/null 2>&1\n    [ $? != 0 ] && echo \"set AUTH_COOKIE_URL failed in etcd\" && exit 1\n}\n\ndo_start_web () {\n    pre_start_web\n\n    DAEMON_OPTS_WEB=\"-p $WEB_PORT\"\n\n    start-stop-daemon --start --background --pidfile $PIDFILE_WEB --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_WEB -- $DAEMON_OPTS_WEB\n    log_end_msg $?\n}\n\ndo_start_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"True\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Starting $DAEMON_NAME_PROXY daemon in $FS_PREFIX\"\n    DAEMON_OPTS_PROXY=\"--port $PROXY_PORT --api-port $PROXY_API_PORT --default-target=http://localhost:8888\"\n    start-stop-daemon --start --background --pidfile $PIDFILE_PROXY --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_PROXY -- $DAEMON_OPTS_PROXY\n    log_end_msg $?\n  }\n\ndo_start_user () {\n    log_daemon_msg \"Starting $DAEMON_NAME_USER in $FS_PREFIX\"\n\n    DAEMON_OPTS_USER=\"-p $USER_PORT\"\n\n    start-stop-daemon --start --background --pidfile $PIDFILE_USER --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_USER_MODULE -- $DAEMON_OPTS_USER\n    log_end_msg $?\n}\n\ndo_stop_master () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_MASTER daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_MASTER --retry 10\n    log_end_msg $?\n}\n\ndo_stop_web () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_WEB daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_WEB --retry 10\n    log_end_msg $?\n}\n\n\ndo_stop_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"True\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Stopping $DAEMON_NAME_PROXY daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_PROXY --retry 10\n    log_end_msg $?\n  }\ndo_stop_user () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_USER daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_USER --retry 10\n    log_end_msg $?\n}\n\ncase \"$1\" in\n    init)\n    \tpre_start_master\n        do_start_master \"new\"\n        do_start_proxy\n\tdo_start_web\n        ;;\n    start)\n    \tpre_start_master\n        do_start_master \"recovery\"\n        do_start_proxy\n\tdo_start_web\n        ;;\n\n    stop)\n        do_stop_master\n        do_stop_proxy\n\tdo_stop_web\n        ;;\n\n    restart)\n        do_stop_master\n        do_stop_proxy\n\tdo_stop_web\n    \tpre_start_master\n        do_start_master \"recovery\"\n        do_start_proxy\n\tdo_start_web\n        ;;\n\n    start_proxy)\n        do_start_proxy\n        ;;\n\n    start_web)\n        do_start_web\n        ;;\n\n    stop_web)\n        do_stop_web\n        ;;\n\n    reinit)\n        do_stop_master\n        do_stop_proxy\n    \tpre_start_master\n        do_start_master \"new\"\n        do_start_proxy\n        ;;\n\n    status)\n        status=0\n        status_of_proc -p $PIDFILE_MASTER \"$DAEMON_MASTER\" \"$DAEMON_NAME_MASTER\" || status=$?\n        status_of_proc -p $PIDFILE_PROXY \"$DAEMON_PROXY\" \"$DAEMON_NAME_PROXY\" || status=$?\n        exit $status\n        ;;\n\n    *)\n        echo \"Usage: $DAEMON_NAME_MASTER {init|start|stop|restart|reinit|status|start_proxy|stop_proxy|start_web|stop_web}\"\n        exit 1\n        ;;\nesac\nexit 0\n"
  },
  {
    "path": "bin/docklet-supermaster",
    "content": "#!/bin/sh\n\n[ $(id -u) != '0' ] && echo \"root is needed\" && exit 1\n\n# get some path of docklet\nbindir=${0%/*}\n# $bindir maybe like /opt/docklet/src/../sbin\n# use command below to make $bindir in normal absolute path\nDOCKLET_BIN=$(cd $bindir; pwd)\nDOCKLET_HOME=${DOCKLET_BIN%/*}\nDOCKLET_CONF=$DOCKLET_HOME/conf\nLXC_SCRIPT=$DOCKLET_CONF/lxc-script\nDOCKLET_SRC=$DOCKLET_HOME/src\nDOCKLET_LIB=$DOCKLET_SRC\nDOCKLET_WEB=$DOCKLET_HOME/web\nDOCKLET_USER=$DOCKLET_HOME/user\n\n# default working directory, default to /opt/docklet\nFS_PREFIX=/opt/docklet\n\n#configurable-http-proxy public port, default is 8000\nPROXY_PORT=8000\n#configurable-http-proxy api port, default is 8001\nPROXY_API_PORT=8001\n#network interface , default is eth0\nNETWORK_DEVICE=eth0\n#etcd server address, default is localhost:2379\nETCD=localhost:2379\n#unique cluster_name, default is docklet-vc\nCLUSTER_NAME=docklet-vc\n#web port, default is 8888\nWEB_PORT=8888\nUSER_PORT=9100\n#cluster net, default is 172.16.0.1/16\nCLUSTER_NET=\"172.16.0.1/16\"\n# ip addresses range of containers for batch job, default is 10.16.0.0/16\nBATCH_NET=\"10.16.0.0/16\"\n\n. $DOCKLET_CONF/docklet.conf\n\nexport FS_PREFIX\n\nRUN_DIR=$FS_PREFIX/local/run\nLOG_DIR=$FS_PREFIX/local/log\n\n# This next line determines what user the script runs as.\nDAEMON_USER=root\n\n# settings for docklet master\nDAEMON_MASTER=$DOCKLET_LIB/master/httprest.py\nDAEMON_NAME_MASTER=docklet-master\nDAEMON_OPTS_MASTER=\n# The process ID of the script when it runs is stored here:\nPIDFILE_MASTER=$RUN_DIR/$DAEMON_NAME_MASTER.pid\n\n# settings for docklet proxy, which is required for web access\nDAEMON_PROXY=`which configurable-http-proxy`\nDAEMON_NAME_PROXY=docklet-proxy\nPIDFILE_PROXY=$RUN_DIR/proxy.pid\nDAEMON_OPTS_PROXY=\n\n# settings for docklet web\nDAEMON_WEB=$DOCKLET_WEB/web.py\nDAEMON_NAME_WEB=docklet-web\nPIDFILE_WEB=$RUN_DIR/docklet-web.pid\nDAEMON_OPTS_WEB=\n\n# settings for docklet user\nDAEMON_USER_MODULE=$DOCKLET_USER/user.py\nDAEMON_NAME_USER=docklet-user\nPIDFILE_USER=$RUN_DIR/docklet-user.pid\nDAEMON_OPTS_USER=\n\nRUNNING_CONFIG=$FS_PREFIX/local/docklet-running.conf\nexport CONFIG=$RUNNING_CONFIG\n\n. /lib/lsb/init-functions\n\n###########\n\npre_start_master () {\n    log_daemon_msg \"Starting $DAEMON_NAME_MASTER in $FS_PREFIX\"\n\n    [ ! -d $FS_PREFIX/global ] && mkdir -p $FS_PREFIX/global\n    [ ! -d $FS_PREFIX/local ] && mkdir -p $FS_PREFIX/local\n    [ ! -d $FS_PREFIX/global/users ] && mkdir -p $FS_PREFIX/global/users\n    [ ! -d $FS_PREFIX/global/sys ] && mkdir -p $FS_PREFIX/global/sys\n    [ ! -d $FS_PREFIX/global/images/private ] && mkdir -p $FS_PREFIX/global/images/private\n    [ ! -d $FS_PREFIX/global/images/public ] && mkdir -p $FS_PREFIX/global/images/public\n    [ ! -d $FS_PREFIX/local/volume ] && mkdir -p $FS_PREFIX/local/volume\n    [ ! -d $FS_PREFIX/local/temp ] && mkdir -p $FS_PREFIX/local/temp\n    [ ! -d $FS_PREFIX/local/run ] && mkdir -p $FS_PREFIX/local/run\n    [ ! -d $FS_PREFIX/local/log ] && mkdir -p $FS_PREFIX/local/log\n\n    grep -P \"^[\\s]*[a-zA-Z]\" $DOCKLET_CONF/docklet.conf > $RUNNING_CONFIG\n\n    echo \"DOCKLET_HOME=$DOCKLET_HOME\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_BIN=$DOCKLET_BIN\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_CONF=$DOCKLET_CONF\" >> $RUNNING_CONFIG\n    echo \"LXC_SCRIPT=$LXC_SCRIPT\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_SRC=$DOCKLET_SRC\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_LIB=$DOCKLET_LIB\" >> $RUNNING_CONFIG\n\n\n    # iptables for NAT network for containers to access web\n    iptables -t nat -F\n    iptables -t nat -A POSTROUTING -s $CLUSTER_NET -j MASQUERADE\n    iptables -t nat -A POSTROUTING -s $BATCH_NET -j MASQUERADE\n\n}\n\ndo_start_master () {\n\n    DAEMON_OPTS_MASTER=$1\n\n\t# MODE : start mode\n\t#    new : clean old data in etcd, global directory and start a new cluster\n\t#    recovery : start cluster and recover status from etcd and global directory\n\t# Default is \"recovery\"\n\n    $DOCKLET_HOME/tools/nginx_config.sh\n\n    start-stop-daemon --start --oknodo --background --pidfile $PIDFILE_MASTER --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_MASTER -- $DAEMON_OPTS_MASTER\n    log_end_msg $?\n}\n\n\ndo_start_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"True\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Starting $DAEMON_NAME_PROXY daemon in $FS_PREFIX\"\n    DAEMON_OPTS_PROXY=\"--port $PROXY_PORT --api-port $PROXY_API_PORT --default-target=http://localhost:8888\"\n    start-stop-daemon --start --background --pidfile $PIDFILE_PROXY --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_PROXY -- $DAEMON_OPTS_PROXY\n    log_end_msg $?\n}\n\npre_start_web () {\n    log_daemon_msg \"Starting $DAEMON_NAME_WEB in $FS_PREFIX\"\n\n    webip=$(ip addr show $NETWORK_DEVICE | grep -oE \"[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+/[0-9]+\")\n\n    [ $? != \"0\" ] && echo \"wrong NETWORK_DEVICE $NETWORK_DEVICE\" && exit 1\n\n    webip=${webip%/*}\n\n    AUTH_COOKIE_URL=http://$webip:$WEB_PORT/jupyter\n    #echo \"set AUTH_COOKIE_URL:$AUTH_COOKIE_URL in etcd with key:$CLUSTER_NAME/web/authurl\"\n    curl -XPUT http://$ETCD/v2/keys/$CLUSTER_NAME/web/authurl -d value=\"$AUTH_COOKIE_URL\" > /dev/null 2>&1\n    [ $? != 0 ] && echo \"set AUTH_COOKIE_URL failed in etcd\" && exit 1\n}\n\ndo_start_web () {\n    pre_start_web\n\n    DAEMON_OPTS_WEB=\"-p $WEB_PORT\"\n\n    start-stop-daemon --start --background --pidfile $PIDFILE_WEB --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_WEB -- $DAEMON_OPTS_WEB\n    log_end_msg $?\n}\n\ndo_start_user () {\n\n    log_daemon_msg \"Starting $DAEMON_NAME_USER in $FS_PREFIX\"\n\n    DAEMON_OPTS_USER=\"-p $USER_PORT\"\n\n    start-stop-daemon --start --background --pidfile $PIDFILE_USER --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_USER_MODULE -- $DAEMON_OPTS_USER\n    log_end_msg $?\n}\n\ndo_stop_master () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_MASTER daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_MASTER --retry 10\n    log_end_msg $?\n}\n\ndo_stop_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"True\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Stopping $DAEMON_NAME_PROXY daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_PROXY --retry 10\n    log_end_msg $?\n}\n\n\ndo_stop_web () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_WEB daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_WEB --retry 10\n    log_end_msg $?\n}\n\ndo_stop_user () {\n    log_daemon_msg \"Stopping $DAEMON_NAME_USER daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_USER --retry 10\n    log_end_msg $?\n}\n\ncase \"$1\" in\n    init)\n    \tpre_start_master\n        do_start_user\n        do_start_proxy\n        do_start_web\n        do_start_master \"new\"\n        ;;\n    start)\n    \tpre_start_master\n        do_start_user\n        do_start_proxy\n        do_start_web\n        do_start_master \"recovery\"\n        ;;\n\n    stop)\n        do_stop_web\n        do_stop_proxy\n        do_stop_master\n        do_stop_user\n        ;;\n\n    restart)\n        do_stop_user\n        do_stop_web\n        do_stop_proxy\n        do_stop_master\n    \tpre_start_master\n        do_start_user\n        do_start_proxy\n        do_start_web\n        do_start_master \"recovery\"\n        ;;\n\n    start_proxy)\n        do_start_proxy\n        ;;\n\n    stop_proxy)\n        do_stop_proxy\n        ;;\n\n    start_web)\n        do_start_web\n        ;;\n\n    stop_web)\n        do_stop_web\n        ;;\n\n    start_user)\n        do_start_user\n        ;;\n\n    stop_user)\n        do_stop_user\n        ;;\n\n    reinit)\n        do_stop_web\n        do_stop_proxy\n        do_stop_master\n        do_stop_user\n    \tpre_start_master\n        do_start_user\n        do_start_proxy\n        do_start_web\n        do_start_master \"new\"\n        ;;\n\n    status)\n        status=0\n        status_of_proc -p $PIDFILE_MASTER \"$DAEMON_MASTER\" \"$DAEMON_NAME_MASTER\" || status=$?\n        status_of_proc -p $PIDFILE_PROXY \"$DAEMON_PROXY\" \"$DAEMON_NAME_PROXY\" || status=$?\n        status_of_proc -p $PIDFILE_WEB \"$DAEMON_WEB\" \"$DAEMON_NAME_WEB\" || status=$?\n        status_of_proc -p $PIDFILE_USER \"$DAEMON_USER\" \"$DAEMON_NAME_USER\" || status=$?\n        exit $status\n        ;;\n\n    *)\n        echo \"Usage: $DAEMON_NAME_MASTER {init|start|stop|restart|reinit|status|start_proxy|stop_proxy|start_web|stop_web}\"\n        exit 1\n        ;;\nesac\nexit 0\n"
  },
  {
    "path": "bin/docklet-worker",
    "content": "#!/bin/sh\n\n[ $(id -u) != '0' ] && echo \"root is needed\" && exit 1\n\n# get some path of docklet\n\nbindir=${0%/*}\n# $bindir maybe like /opt/docklet/src/../bin\n# use command below to make $bindir in normal absolute path\nDOCKLET_BIN=$(cd $bindir; pwd)\nDOCKLET_HOME=${DOCKLET_BIN%/*}\nDOCKLET_CONF=$DOCKLET_HOME/conf\nLXC_SCRIPT=$DOCKLET_CONF/lxc-script\nDOCKLET_SRC=$DOCKLET_HOME/src\nDOCKLET_LIB=$DOCKLET_SRC\nDOCKLET_WEB=$DOCKLET_HOME/web\n\n# working directory, default to /opt/docklet\nFS_PREFIX=/opt/docklet\n\n# cluster net ip range,  default is 172.16.0.1/16\nCLUSTER_NET=\"172.16.0.1/16\"\n# ip addresses range of containers for batch job, default is 10.16.0.0/16\nBATCH_NET=\"10.16.0.0/16\"\n#configurable-http-proxy public port, default is 8000\nPROXY_PORT=8000\n#configurable-http-proxy api port, default is 8001\nPROXY_API_PORT=8001\nDISTRIBUTED_GATEWAY=False\n\n. $DOCKLET_CONF/docklet.conf\n\nexport FS_PREFIX\n\nRUN_DIR=$FS_PREFIX/local/run\nLOG_DIR=$FS_PREFIX/local/log\n\n# This next line determines what user the script runs as.\nDAEMON_USER=root\n\n# settings for docklet worker\nDAEMON=$DOCKLET_LIB/worker/worker.py\nDAEMON_NAME=docklet-worker\nDAEMON_OPTS=\n# The process ID of the script when it runs is stored here:\nPIDFILE=$RUN_DIR/$DAEMON_NAME.pid\n\n# settings for docklet batch worker, which is required for batch job processing system\nBATCH_ON=True\nDAEMON_BATCH=$DOCKLET_LIB/worker/taskworker.py\nDAEMON_NAME_BATCH=docklet-taskworker\nPIDFILE_BATCH=$RUN_DIR/batch.pid\nDAEMON_OPTS_BATCH=\n\n# settings for docklet proxy, which is required for web access\nDAEMON_PROXY=`which configurable-http-proxy`\nDAEMON_NAME_PROXY=docklet-proxy\nPIDFILE_PROXY=$RUN_DIR/proxy.pid\nDAEMON_OPTS_PROXY=\n\nDOCKMETER_NAME=$DAEMON_NAME-metering\nDOCKMETER_PIDFILE=$RUN_DIR/$DOCKMETER_NAME.pid\n\n. /lib/lsb/init-functions\n\n###########\n\nupdate_container_conf () {\n    LXC_VERSION=$(lxc-start --version | awk -F \".\" '{print $1}')\n    #echo $LXC_VERSION\n\n    if [ \"$LXC_VERSION\"x != \"2\"x ]&&[ \"$LXC_VERSION\"x != \"3\"x ];then\n      LXC_VERSION=2\n    fi\n    #echo $LXC_VERSION\n\n    cp $DOCKLET_CONF/container/lxc$LXC_VERSION.container.conf $DOCKLET_CONF/container.conf\n    cp $DOCKLET_CONF/container/lxc$LXC_VERSION.container.batch.conf $DOCKLET_CONF/container.batch.conf\n    #echo \"cp $DOCKLET_CONF/container/lxc$LXC_VERSION.container.conf $DOCKLET_CONF/container.conf\"\n}\n\npre_start () {\n    [ ! -d $FS_PREFIX/global ] && mkdir -p $FS_PREFIX/global\n    [ ! -d $FS_PREFIX/local ] && mkdir -p $FS_PREFIX/local\n    [ ! -d $FS_PREFIX/global/users ] && mkdir -p $FS_PREFIX/global/users\n    [ ! -d $FS_PREFIX/local/volume ] && mkdir -p $FS_PREFIX/local/volume\n    [ ! -d $FS_PREFIX/local/temp ] && mkdir -p $FS_PREFIX/local/temp\n    [ ! -d $FS_PREFIX/local/run ] && mkdir -p $FS_PREFIX/local/run\n    [ ! -d $FS_PREFIX/local/log ] && mkdir -p $FS_PREFIX/local/log\n\n    tempdir=/opt/docklet/local/temp\n\n    RUNNING_CONFIG=$FS_PREFIX/local/docklet-running.conf\n\n    grep -P \"^[\\s]*[a-zA-Z]\" $DOCKLET_CONF/docklet.conf > $RUNNING_CONFIG\n\n    echo \"DOCKLET_HOME=$DOCKLET_HOME\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_BIN=$DOCKLET_BIN\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_CONF=$DOCKLET_CONF\" >> $RUNNING_CONFIG\n    echo \"LXC_SCRIPT=$LXC_SCRIPT\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_SRC=$DOCKLET_SRC\" >> $RUNNING_CONFIG\n    echo \"DOCKLET_LIB=$DOCKLET_LIB\" >> $RUNNING_CONFIG\n\n    export CONFIG=$RUNNING_CONFIG\n\n    # iptables for NAT network for containers to access web\n    iptables -t nat -F\n    iptables -t nat -A POSTROUTING -s $CLUSTER_NET -j MASQUERADE\n    iptables -t nat -A POSTROUTING -s $BATCH_NET -j MASQUERADE\n\n\tif [ ! -d $FS_PREFIX/local/basefs ]; then\n\t\tlog_daemon_msg \"basefs does not exist, run prepare.sh first\" && exit 1\n\tfi\n\n\tif [ ! -d $FS_PREFIX/local/packagefs ]; then\n\t\tmkdir -p $FS_PREFIX/local/packagefs\n\tfi\n\n  update_container_conf\n}\n\ndo_start() {\n    pre_start\n\n    DAEMON_OPTS=$1\n    log_daemon_msg \"Starting $DAEMON_NAME in $FS_PREFIX\"\n    #python3 $DAEMON\n    start-stop-daemon --start --oknodo --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS\n    log_end_msg $?\n}\n\ndo_start_batch () {\n    if [ \"$BATCH_ON\" = \"False\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Starting $DAEMON_NAME_BATCH in $FS_PREFIX\"\n\n    DAEMON_OPTS_BATCH=\"\"\n\n    start-stop-daemon --start --background --pidfile $PIDFILE_BATCH --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_BATCH -- $DAEMON_OPTS_BATCH\n    log_end_msg $?\n}\n\ndo_start_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"False\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Starting $DAEMON_NAME_PROXY daemon in $FS_PREFIX\"\n    DAEMON_OPTS_PROXY=\"--port $PROXY_PORT --api-port $PROXY_API_PORT --default-target=http://localhost:8888\"\n    start-stop-daemon --start --background --pidfile $PIDFILE_PROXY --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON_PROXY -- $DAEMON_OPTS_PROXY\n    log_end_msg $?\n}\n\ndo_stop () {\n    log_daemon_msg \"Stopping $DAEMON_NAME daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE --retry 10\n    log_end_msg $?\n}\n\ndo_stop_batch () {\n    if [ \"$BATCH_ON\" = \"False\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Stopping $DAEMON_NAME_BATCH daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_BATCH --retry 10\n    log_end_msg $?\n}\n\ndo_stop_proxy () {\n    if [ \"$DISTRIBUTED_GATEWAY\" = \"False\" ]\n    then\n        return 1\n    fi\n    log_daemon_msg \"Stopping $DAEMON_NAME_PROXY daemon\"\n    start-stop-daemon --stop --quiet --oknodo --remove-pidfile --pidfile $PIDFILE_PROXY --retry 10\n    log_end_msg $?\n}\n\ndo_start_meter() {\n    log_daemon_msg \"Starting $DOCKMETER_NAME in $FS_PREFIX\"\n    start-stop-daemon --start --background --pidfile $DOCKMETER_PIDFILE --make-pidfile --exec $DOCKLET_HOME/meter/main.py\n    log_end_msg $?\n}\n\ndo_stop_meter() {\n    log_daemon_msg \"Stopping $DOCKMETER_NAME daemon\"\n    start-stop-daemon --stop --pidfile $DOCKMETER_PIDFILE --remove-pidfile\n    log_end_msg $?\n}\n\n\n\n\ncase \"$1\" in\n    start)\n        do_start \"normal-worker\"\n        do_start_batch\n        do_start_proxy\n        ;;\n\n    stop)\n        do_stop\n        do_stop_batch\n        do_stop_proxy\n        ;;\n    start-meter)\n        do_start_meter\n        ;;\n\n    stop-meter)\n        do_stop_meter\n        ;;\n\n    start_batch)\n\tdo_start \"batch-worker\"\n        do_start_batch\n        ;;\n\n    stop_batch)\n\tdo_stop\n        do_stop_batch\n        ;;\n\n    start_proxy)\n        do_start_proxy\n        ;;\n\n    stop_proxy)\n        do_stop_proxy\n        ;;\n\n    console)\n        pre_start\n        cprofilev $DAEMON $DAEMON_OPTS\n        ;;\n\n    restart)\n        do_stop\n        do_stop_batch\n        do_stop_proxy\n        do_start \"normal-worker\"\n        do_start_batch\n        do_start_proxy\n        ;;\n\n    status)\n        status_of_proc -p $PIDFILE \"$DAEMON\" \"$DAEMON_NAME\" && exit 0 || exit $?\n        status_of_proc -p $PIDFILE_BATCH \"$DAEMON_BATCH\" \"$DAEMON_NAME_BATCH\" || status=$?\n        status_of_proc -p $PIDFILE_PROXY \"$DAEMON_PROXY\" \"$DAEMON_NAME_PROXY\" || status=$?\n        ;;\n    *)\n        echo \"Usage: $DAEMON_NAME {start|stop|restart|status}\"\n        exit 1\n        ;;\nesac\nexit 0\n"
  },
  {
    "path": "cloudsdk-installer.sh",
    "content": "#!/bin/bash\n\nif [[ \"`whoami`\" != \"root\" ]]; then\n\techo \"FAILED: Require root previledge !\" > /dev/stderr\n\texit 1\nfi\n\npip3 install aliyun-python-sdk-core-v3\npip3 install aliyun-python-sdk-ecs\n\nexit 0\n"
  },
  {
    "path": "conf/container/lxc2.container.batch.conf",
    "content": "# This is the common container.conf for all containers.\n# If want set custom settings, you have two choices:\n# 1. Directly modify this file, which is not recommend, because the\n#    setting will be overriden when new version container.conf released.\n# 2. Use a custom config file in this conf directory: lxc.custom.conf,\n#    it uses the same grammer as container.conf, and will be merged\n#    with the default container.conf by docklet at runtime.\n#\n#   The following is an example mounting user html directory\n#   lxc.mount.entry = /public/home/%USERNAME%/public_html %ROOTFS%/root/public_html none bind,rw,create=dir 0 0\n#\n\n#### include /usr/share/lxc/config/ubuntu.common.conf\nlxc.include = /usr/share/lxc/config/ubuntu.common.conf\n\n############## DOCKLET CONFIG ##############\n\n# Setup 0 tty devices\nlxc.tty = 0\n\nlxc.rootfs = %ROOTFS%\nlxc.utsname = %HOSTNAME%\n\nlxc.network.type = veth\nlxc.network.name = eth0\n# veth.pair is limited in 16 bytes\nlxc.network.veth.pair = %VETHPAIR%\nlxc.network.script.up = %LXCSCRIPT%/lxc-ifup\nlxc.network.script.down = %LXCSCRIPT%/lxc-ifdown\nlxc.network.ipv4 = %IP%\nlxc.network.ipv4.gateway = %GATEWAY%\nlxc.network.flags = up\nlxc.network.mtu = 1420\n\nlxc.cgroup.pids.max = 2048\nlxc.cgroup.memory.limit_in_bytes = %CONTAINER_MEMORY%M\n#lxc.cgroup.memory.kmem.limit_in_bytes = 512M\n#lxc.cgroup.memory.soft_limit_in_bytes = 4294967296\n#lxc.cgroup.memory.memsw.limit_in_bytes = 8589934592\n\n# lxc.cgroup.cpu.cfs_period_us : period time of cpu, default 100000, means 100ms\n# lxc.cgroup.cpu.cfs_quota_us  : quota time of this process\nlxc.cgroup.cpu.cfs_quota_us = %CONTAINER_CPU%\n\nlxc.cap.drop = sys_admin net_admin mac_admin mac_override sys_time sys_module\n\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/data %ROOTFS%/root/nfs none bind,rw,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/hosts/batch-%TASKID%.hosts %ROOTFS%/etc/hosts none bind,ro,create=file 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/ssh %ROOTFS%/root/.ssh none bind,ro,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/local/temp/%LXCNAME%/ %ROOTFS%/tmp none bind,rw,create=dir 0 0\n\n# setting hostname\nlxc.hook.pre-start = %LXCSCRIPT%/lxc-prestart\n\n# setting nfs softlink\n#lxc.hook.mount = %LXCSCRIPT%/lxc-mount\n"
  },
  {
    "path": "conf/container/lxc2.container.conf",
    "content": "# This is the common container.conf for all containers.\n# If want set custom settings, you have two choices:\n# 1. Directly modify this file, which is not recommend, because the\n#    setting will be overriden when new version container.conf released.\n# 2. Use a custom config file in this conf directory: lxc.custom.conf,\n#    it uses the same grammer as container.conf, and will be merged\n#    with the default container.conf by docklet at runtime.\n#\n#   The following is an example mounting user html directory\n#   lxc.mount.entry = /public/home/%USERNAME%/public_html %ROOTFS%/root/public_html none bind,rw,create=dir 0 0\n#\n\n#### include /usr/share/lxc/config/ubuntu.common.conf\nlxc.include = /usr/share/lxc/config/ubuntu.common.conf\n\n############## DOCKLET CONFIG ##############\n\n# Setup 0 tty devices\nlxc.tty = 0\n\nlxc.rootfs = %ROOTFS%\nlxc.utsname = %HOSTNAME%\n\nlxc.network.type = veth\nlxc.network.name = eth0\n# veth.pair is limited in 16 bytes\nlxc.network.veth.pair = %VETHPAIR%\nlxc.network.script.up = %LXCSCRIPT%/lxc-ifup\nlxc.network.script.down = %LXCSCRIPT%/lxc-ifdown\nlxc.network.ipv4 = %IP%\nlxc.network.ipv4.gateway = %GATEWAY%\nlxc.network.flags = up\nlxc.network.mtu = 1420\n\nlxc.cgroup.pids.max = 2048\nlxc.cgroup.memory.limit_in_bytes = %CONTAINER_MEMORY%M\n#lxc.cgroup.memory.kmem.limit_in_bytes = 512M\n#lxc.cgroup.memory.soft_limit_in_bytes = 4294967296\n#lxc.cgroup.memory.memsw.limit_in_bytes = 8589934592\n\n# lxc.cgroup.cpu.cfs_period_us : period time of cpu, default 100000, means 100ms\n# lxc.cgroup.cpu.cfs_quota_us  : quota time of this process\nlxc.cgroup.cpu.cfs_quota_us = %CONTAINER_CPU%\n\nlxc.cap.drop = sys_admin net_admin mac_admin mac_override sys_time sys_module\n\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/data %ROOTFS%/root/nfs none bind,rw,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/hosts/%CLUSTERID%.hosts %ROOTFS%/etc/hosts none bind,ro,create=file 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/ssh %ROOTFS%/root/.ssh none bind,ro,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/local/temp/%LXCNAME%/ %ROOTFS%/tmp none bind,rw,create=dir 0 0\n\n# setting hostname\nlxc.hook.pre-start = %LXCSCRIPT%/lxc-prestart\n\n# setting nfs softlink\n#lxc.hook.mount = %LXCSCRIPT%/lxc-mount\n"
  },
  {
    "path": "conf/container/lxc3.container.batch.conf",
    "content": "# This is the common container.conf for all containers.\n# If want set custom settings, you have two choices:\n# 1. Directly modify this file, which is not recommend, because the\n#    setting will be overriden when new version container.conf released.\n# 2. Use a custom config file in this conf directory: lxc.custom.conf,\n#    it uses the same grammer as container.conf, and will be merged\n#    with the default container.conf by docklet at runtime.\n#\n#   The following is an example mounting user html directory\n#   lxc.mount.entry = /public/home/%USERNAME%/public_html %ROOTFS%/root/public_html none bind,rw,create=dir 0 0\n#\n\n#### include /usr/share/lxc/config/ubuntu.common.conf\nlxc.include = /usr/share/lxc/config/ubuntu.common.conf\n\n############## DOCKLET CONFIG ##############\n\n# Setup 0 tty devices\nlxc.tty.max = 0\n\nlxc.rootfs.path = %ROOTFS%\nlxc.uts.name = %HOSTNAME%\n\nlxc.net.0.type = veth\nlxc.net.0.name = eth0\n# veth.pair is limited in 16 bytes\nlxc.net.0.veth.pair = %VETHPAIR%\nlxc.net.0.script.up = %LXCSCRIPT%/lxc-ifup\nlxc.net.0.script.down = %LXCSCRIPT%/lxc-ifdown\nlxc.net.0.ipv4.address = %IP%\nlxc.net.0.ipv4.gateway = %GATEWAY%\nlxc.net.0.flags = up\nlxc.net.0.mtu = 1420\n\nlxc.cgroup.pids.max = 2048\nlxc.cgroup.memory.limit_in_bytes = %CONTAINER_MEMORY%M\n#lxc.cgroup.memory.kmem.limit_in_bytes = 512M\n#lxc.cgroup.memory.soft_limit_in_bytes = 4294967296\n#lxc.cgroup.memory.memsw.limit_in_bytes = 8589934592\n\n# lxc.cgroup.cpu.cfs_period_us : period time of cpu, default 100000, means 100ms\n# lxc.cgroup.cpu.cfs_quota_us  : quota time of this process\nlxc.cgroup.cpu.cfs_quota_us = %CONTAINER_CPU%\n\nlxc.cap.drop = sys_admin net_admin mac_admin mac_override sys_time sys_module\n\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/data %ROOTFS%/root/nfs none bind,rw,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/hosts/batch-%TASKID%.hosts %ROOTFS%/etc/hosts none bind,ro,create=file 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/ssh %ROOTFS%/root/.ssh none bind,ro,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/local/temp/%LXCNAME%/ %ROOTFS%/tmp none bind,rw,create=dir 0 0\n\n# setting hostname\nlxc.hook.pre-start = %LXCSCRIPT%/lxc-prestart\n\n# setting nfs softlink\n#lxc.hook.mount = %LXCSCRIPT%/lxc-mount\n"
  },
  {
    "path": "conf/container/lxc3.container.conf",
    "content": "# This is the common container.conf for all containers.\n# If want set custom settings, you have two choices:\n# 1. Directly modify this file, which is not recommend, because the\n#    setting will be overriden when new version container.conf released.\n# 2. Use a custom config file in this conf directory: lxc.custom.conf,\n#    it uses the same grammer as container.conf, and will be merged\n#    with the default container.conf by docklet at runtime.\n#\n#   The following is an example mounting user html directory\n#   lxc.mount.entry = /public/home/%USERNAME%/public_html %ROOTFS%/root/public_html none bind,rw,create=dir 0 0\n#\n\n#### include /usr/share/lxc/config/ubuntu.common.conf\nlxc.include = /usr/share/lxc/config/ubuntu.common.conf\n\n############## DOCKLET CONFIG ##############\n\n# Setup 0 tty devices\nlxc.tty.max = 0\n\nlxc.rootfs.path = %ROOTFS%\nlxc.uts.name = %HOSTNAME%\n\nlxc.net.0.type = veth\nlxc.net.0.name = eth0\n# veth.pair is limited in 16 bytes\nlxc.net.0.veth.pair = %VETHPAIR%\nlxc.net.0.script.up = %LXCSCRIPT%/lxc-ifup\nlxc.net.0.script.down = %LXCSCRIPT%/lxc-ifdown\nlxc.net.0.ipv4.address = %IP%\nlxc.net.0.ipv4.gateway = %GATEWAY%\nlxc.net.0.flags = up\nlxc.net.0.mtu = 1420\n\nlxc.cgroup.pids.max = 2048\nlxc.cgroup.memory.limit_in_bytes = %CONTAINER_MEMORY%M\n#lxc.cgroup.memory.kmem.limit_in_bytes = 512M\n#lxc.cgroup.memory.soft_limit_in_bytes = 4294967296\n#lxc.cgroup.memory.memsw.limit_in_bytes = 8589934592\n\n# lxc.cgroup.cpu.cfs_period_us : period time of cpu, default 100000, means 100ms\n# lxc.cgroup.cpu.cfs_quota_us  : quota time of this process\nlxc.cgroup.cpu.cfs_quota_us = %CONTAINER_CPU%\n\nlxc.cap.drop = sys_admin net_admin mac_admin mac_override sys_time sys_module\n\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/data %ROOTFS%/root/nfs none bind,rw,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/hosts/%CLUSTERID%.hosts %ROOTFS%/etc/hosts none bind,ro,create=file 0 0\nlxc.mount.entry = %FS_PREFIX%/global/users/%USERNAME%/ssh %ROOTFS%/root/.ssh none bind,ro,create=dir 0 0\nlxc.mount.entry = %FS_PREFIX%/local/temp/%LXCNAME%/ %ROOTFS%/tmp none bind,rw,create=dir 0 0\n\n# setting hostname\nlxc.hook.pre-start = %LXCSCRIPT%/lxc-prestart\n\n# setting nfs softlink\n#lxc.hook.mount = %LXCSCRIPT%/lxc-mount\n"
  },
  {
    "path": "conf/docklet.conf.template",
    "content": "\n# ==================================================\n#\n# [Local config example]\n#\n# ==================================================\n\n# CLUSTER_NAME: name of host cluster, every host cluster should have\n# a unique name, default is docklet-vc\n# CLUSTER_NAME=docklet-vc\n\n# FS_PREFIX: path to store global and local data for docklet\n# default is /opt/docklet.\n#\n# Note: $FS_PREFIX/global is for storing persistent data, e.g.,\n# custom container images, user data, etc. For a multi hosts\n# environement, it is the mountpoint of the distributed filesystem\n# that all physical hosts (master and slave) share.\n# E.g., for a system with three hosts: computing hosts A and B,\n# strorage host C. Host C exports its stroage filesystem through nfs\n# as C:/data, then host A and B should mount C:/data to $FS_PREFIX/global.\n# Please make sure that the mount is OK before launching docklet.\n#\n# FS_PREFIX=/opt/docklet\n\n# STORAGE: local storage type, file or disk, default is file\n#  note lvm is required for either case\n#\n# file : a large file simulating raw disk storing container runtime\n# data, located in FS_PREFIX/local, for single machine testing purpose.\n#\n# disk : raw disk for storing container files, for production purpose.\n# If using disk, a partition must be allocated to docklet\n# - a disk device name must be specified by DISK , e.g, /dev/sdc9\n# - this device must be formatted as Linux-LVM, and initialized\n# as a physical volume (pvcreate /dev/sdc9) in advance.\n# TAKE CARE to ensure the disk is OK before launching docklet.\n#\n# STORAGE=file\n#\n# DISK: disk device name if STORAGE is disk\n# DISK=/dev/sdc9\n\n# CLUSTER_SIZE: virtual cluster size, default is 1\n# CLUSTER_SIZE=1\n\n# CLUSTER_NET: cluster network ip address range, default is 172.16.0.1/16\n# CLUSTER_NET=172.16.0.1/16\n\n# Deprecated since v0.2.7. read from quota group set in web admin page\n# CONTAINER_CPU: CPU quota of container, default is 100000\n# A single CPU core has total=100000 (100ms), so the default 100000\n# mean a single container can occupy a whole core.\n# For a CPU with two cores, this can be set to 200000\n# CONTAINER_CPU=100000\n\n# Deprecated since v0.2.7. read from quota group set in web admin page\n# CONTAINER_DISK: disk quota of container image upper layer, count in MB,\n# default is 1000\n# CONTAINER_DISK=1000\n\n# Deprecated since v0.2.7. read from quota group set in web admin page\n# CONTAINER_MEMORY: memory quota of container, count in MB, default is 1000\n# CONTAINER_MEMORY=1000\n\n# DISKPOOL_SIZE: lvm group size, count in MB, default is 10000\n# Only valid with STORAGE=file\n# DISKPOOL_SIZE=10000\n\n# ETCD: etcd address, default is localhost:2379\n# For a muti hosts environment, the administrator should configure how\n# etcd cluster work together\n# ETCD=localhost:2379\n\n# NETWORK_DEVICE: specify the network interface docklet uses,\n# Default is eth0\n# NETWORK_DEVICE=eth0\n\n# PORTAL_URL: the public docklet portal url. for a production system,\n# it should be a valid URL, like http://docklet.info\n# default is MASTER_IP:NGINX_PORT\n# PORTAL_URL=http://localhost:8080\n\n# MASTER_IP: master listen ip, default listens on all interfaces\n# MASTER_IP=0.0.0.0\n\n# MASTER_PORT: master listen port, default is 9000\n# MASTER_PORT=9000\n\n# WORKER_PORT: worker listen port, default is 9001\n# WORKER_PORT=9001\n\n# NGINX_PORT: the access port of the public portal, default is 8080\n# This is the listening port of nginx server. The nginx server forwards\n# requests according to the requests' urls. If the urls are to workspaces,\n# it will forward requests to the configurable-http-proxy, otherwise,\n# to the docklet web. Usually 80 is recommded for production environment\n# NGINX_PORT=8080\n\n# PROXY_PORT: the listening port of configurable-http-proxy, default is 8000\n# it proxy connections from exteral public network to internal private\n# container networks.\n# PROXY_PORT=8000\n\n# PROXY_API_PORT: configurable-http-proxy api port, default is 8001\n# Admins can query the proxy table by calling:\n# curl http://localhost:8001/api/routes\n# PROXY_API_PORT=8001\n\n# WEB_PORT: docklet web listening port, default is 8888\n# Note: docklet web server is located behind the docklet proxy.\n# Users access docklet first through proxy, then docklet web server.\n# Therefore, it is not for user direct access.  In most cases,\n# admins need not to change the default value.\n# WEB_PORT=8888\n\n# LOG_LEVEL: logging level, of DEBUG, INFO, WARNING, ERROR, CRITICAL\n# default is DEBUG\n# LOG_LEVEL=DEBUG\n\n# LOG_LIFE: how many days the logs will be kept, default is 10\n# LOG_LIFE=10\n\n# WEB_LOG_LEVEL: logging level, of DEBUG, INFO, WARNING, ERROR, CRITICAL\n# default is DEBUG\n# WEB_LOG_LEVEL=DEBUG\n\n# EXTERNAL_LOGIN: whether docklet will use external account to log in\n# True or False, default is False\n# default: authenticate local and PAM users\n# EXTERNAL_LOGIN=False\n\n# DATA_QUOTA : whether enable the quota of data volume or not\n# True or False, default: False\n# DATA_QUOTA=False\n\n# DATA_QUOTA_CMD : the cmd to set the quota of a given directory. It accepts two arguments:\n# arg1: the directory name, relative path from the data volume root, e.g, \"/users/bob/data\"\n# arg2: the quota value in GB of string, e.g., \"100\"\n# default: \"gluster volume quota docklet-volume limit-usage %s %s\"\n# DATA_QUOTA_CMD=\"gluster volume quota docklet-volume limit-usage %s %s\"\n\n# DISTRIBUTED_GATEWAY : whether the users' gateways are distributed or not\n# Must be set by same value on master and workers.\n# True or False, default: False\n# DISTRIBUTED_GATEWAY=False\n\n# PUBLIC_IP : publick ip of this machine. If DISTRIBUTED_GATEWAY is True,\n# users' gateways can be setup on this machine. Users can visit this machine\n# by the public ip. default: IP of NETWORK_DEVICE.\n# PUBLIC_IP=0.0.0.0\n\n# NGINX_CONF: the config path of nginx, default: /etc/nginx\n# NGINX_CONF=/etc/nginx\n\n# MASTER_IPS: all master ips in a cente, depart by ','.\n# e.g:192.168.192.191@master1,192.168.192.192@master2\n# you can also add description to each master.\n# e.g:master1_desc=\"this is master1\"\n# defalut:0.0.0.0@docklet\n# MASTER_IPS=0.0.0.0@docklet\n\n# USER_IP: user listen ip\n# default:0.0.0.0\n# USER_IP=0.0.0.0\n\n# USER_PORT: user listen port\n# default:9100\n# USER_PORT=9100\n\n# AUTH_KEY: the key to request users server from master,\n# or to request master from users server. Please set the\n# same value on each machine. Please don't use the default value.\n# AUTH_KEY=docklet\n\n# ALLOCATED_PORTS: the ports on this host that will be allocated to users.\n# The allocated ports are for ports mapping. Default: 10000-65535\n# The two ports next to '-' are inclueded. If there are several ranges,\n# Please seperate them by ',' , for example: 10000-20000,30000-40000\n# ALLOCATED_PORTS=10000-65535\n\n# ALLOW_SCALE_OUT: allow docklet to rent server on the cloud to scale out\n# Only when you deploy docklet on the cloud can you set it to True\n# ALLOW_SCALE_OUT=False\n\n# WARNING_DAYS: user will receive a warning email for releasing\n# when his/her vcluster has been stopped for more then the days.\n# Default: 7\n# WARNING_DAYS=7\n\n# RELEASE_DAYS: the vcluster will be released when it has been\n# stopped for more then the days. Needs to be larger then WARNING_DAYS.\n# Default: 14\n# RELEASE_DAYS=14\n\n# ==================================================\n#\n# Batch Config\n#\n# ==================================================\n\n# BATCH_ON: whether to start batch job processing system when start\n# the docklet. Default: True\n# BATCH_ON=True\n\n# BATCH_MASTER_PORT: the rpc server port on master.\n# default: 50050\n# BATCH_MASTER_PORT=50050\n\n# BATCH_WORKER_PORT: the rpc server port on worker.\n# default: 50051\n# BATCH_WORKER_PORT=50051\n\n# BATCH_NET: ip addresses range of containers for batch job, default is 10.16.0.0/16\n# BATCH_NET=10.16.0.0/16\n\n# BATCH_TASK_CIDR: 2^(BATCH_TASK_CIDR)-2 is the number of ip addresses for a task, default is 4\n# BATCH_TASK_CIDR=4\n\n# BATCH_MAX_THREAD_WORKER: the maximun number of threads of the rpc server on\n# the batch job worker. default:5\n# BATCH_MAX_THREAD_WORKER=5\n\n# BATCH_GPU_BILLING: beans cost per hour by different GPUs\n# The GPU's name can be found by 'nvidia-smi -L' and all spaces need be replaced by '-'\n# default: 100\n# BATCH_GPU_BILLING=default:100,GeForce-GTX-1080-Ti:100,GeForce-GTX-2080-Ti:150,Tesla-V100-PCIE-16GB:200"
  },
  {
    "path": "conf/lxc-script/lxc-ifdown",
    "content": "#!/bin/sh\n\n# $1 : name of container ( name in lxc-start with -n)\n# $2 : net\n# $3 : network flags, up or down\n# $4 : network type, for example, veth\n# $5 : value of lxc.network.veth.pair\n\n. $LXC_ROOTFS_PATH/../env.conf\n\novs-vsctl --if-exists del-port $Bridge $5\ncnt=$(ovs-vsctl list-ports ${Bridge} | wc -l)\nif [ \"$cnt\" = \"1\" ]; then\n  greport=$(ovs-vsctl list-ports ${Bridge} | grep \"gre\" | wc -l)\n  if [ \"$greport\" = \"1\" ]; then\n    ovs-vsctl del-br $Bridge\n  fi\nfi\n"
  },
  {
    "path": "conf/lxc-script/lxc-ifup",
    "content": "#!/bin/sh\n\n\n# $1 : name of container ( name in lxc-start with -n)\n# $2 : net\n# $3 : network flags, up or down\n# $4 : network type, for example, veth\n# $5 : value of lxc.network.veth.pair\n\n. $LXC_ROOTFS_PATH/../env.conf\n\novs-vsctl --may-exist add-br $Bridge\novs-vsctl --may-exist add-port $Bridge $5\n"
  },
  {
    "path": "conf/lxc-script/lxc-mount",
    "content": "#!/bin/sh\n\n# $1 Container name.\n# $2 Section (always 'lxc').\n# $3 The hook type (i.e. 'clone' or 'pre-mount').\n\n#cd $LXC_ROOTFS_PATH/root ; rm -rf nfs &&  ln -s ../nfs nfs\n"
  },
  {
    "path": "conf/lxc-script/lxc-prestart",
    "content": "#!/bin/sh\n\n# $1 Container id\n# $2 Container name.\n# $3 Section (always 'lxc').\n# $4 The hook type (i.e. 'clone' or 'pre-mount').\n\n# following environment variables are set by lxc :\n# $LXC_NAME: is the container's name.\n# $LXC_ROOTFS_MOUNT: the path to the mounted root filesystem.\n# $LXC_CONFIG_FILE: the path to the container configuration file.\n# $LXC_SRC_NAME: in the case of the clone hook, this is the original container's name.\n# $LXC_ROOTFS_PATH: this is the lxc.rootfs entry for the container.\n#                   Note this is likely not where the mounted rootfs is to be found, use LXC_ROOTFS_MOUNT for that.\n\n. $LXC_ROOTFS_PATH/../env.conf\n\necho $HNAME > $LXC_ROOTFS_PATH/etc/hostname\n"
  },
  {
    "path": "conf/nginx_docklet.conf",
    "content": "server\n{\n         listen %NGINX_PORT;\n         #ssl on;\n         #ssl_certificate /etc/nginx/ssl/server.crt;\n         #ssl_certificate_key /etc/nginx/ssl/server.key;\n         #ssl_protocols  TLSv1.2 TLSv1.3;\n         #ssl_prefer_server_ciphers on;\n         #ssl_ciphers TLS13-AES-128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;\n\n         server_name    nginx_docklet.conf;\n         charset UTF-8;\n         add_header X-Frame-Options SAMEORIGIN;\n         merge_slashes off;\n         rewrite (.*)//+(.*) $1/$2 permanent;\n         index index.html index.htm;\n         client_max_body_size 20m;\n         if ($request_method ~* OPTIONS){\n              return 403;\n         }\n         location ~ ^/NginxStatus/ {\n              stub_status on;\n              access_log off;\n         }\n\n         location ~ ^/(\\d+\\.\\d+\\.\\d+\\.\\d+)/ {\n               proxy_pass    http://$1:%PROXY_PORT;\n               proxy_set_header Host $host;\n               proxy_http_version 1.1;\n               proxy_set_header Upgrade $http_upgrade;\n               proxy_set_header Connection \"Upgrade\";\n         }\n\n         location / {\n             client_max_body_size 20m;\n             client_body_buffer_size 256k;\n             proxy_connect_timeout 300;\n             proxy_send_timeout 300;\n             proxy_read_timeout 300;\n             proxy_buffer_size 256k;\n             proxy_buffers 4 256k;\n             proxy_busy_buffers_size 256k;\n             proxy_temp_file_write_size 256k;\n             proxy_next_upstream error timeout invalid_header http_500 http_503 http_404;\n             proxy_max_temp_file_size 128m;\n             proxy_ignore_client_abort on;\n             proxy_pass    http://%MASTER_IP:%WEB_PORT;\n             proxy_http_version 1.1;\n             proxy_set_header Upgrade $http_upgrade;\n             proxy_set_header Connection \"Upgrade\";\n            }\n}\n"
  },
  {
    "path": "doc/devdoc/coding.md",
    "content": "# NOTE\n\n## here is some thinking and notes in coding\n\n* path : scripts' path should be known by scripts to call/import other script -- use environment variables\n\n* FS_PREFIX : docklet filesystem path to put data\n\n* overlay : \" modprobe overlay \" to add overlay module\n\n* after reboot :\n    * bridges lost -- it's ok, recreate it\n    * loop device lost -- losetup /dev/loop0 BLOCK_FILE again, and lvm will get group and volume back automatically\n\n* lvm can do snapshot, image management can use lvm's snapshot -- No! lvm snapshot will use the capacity of LVM group.\n\n* cgroup memory control maybe not work. need run command below:\n        echo 'GRUB_CMDLINE_LINUX=\"cgroup_enable=memory swapaccount=1\"' >> /etc/default/grub && update-grub && reboot\n\n* debian don't support cpu.cfs_quota_us option in cgroup. it needs to recompile linux kernel with CONFIG_CFS_BANDWIDTH option\n\n* ip can add bridge/link/GRE, maybe we should test whether ip can replace of ovs-vsctl and brctl. ( see \"man ip-link\" )\n\n* lxc.mount.entry :\n\t* do not use relevant path. use absolute path, like :\n\t\t\tlxc.mount.entry = /root/from-dir /root/rootfs/to-dir none bind 0 0         # lxc.rootfs = /root/rootfs\n\t\tif use relevant paht, container path will be mounted on /usr/lib/x86_64..../ , a not existed path\n\t* path of host and container should both exist. if not exist in container, it will be mounted on /usr/lib/x86_64....\n\t* if path in container not exists, you can use option : create=dir/file, like :\n\t\t\tlxc.mount.entry = /root/from-dir /root/rootfs/to-dir none bind,create=dir 0 0  # lxc.rootfs = /root/rootfs\n\n* lxc.mount.entry : bind and rbind ( see \"man mount\" )\n\t* bind means mount a part of filesystem on somewhere else of this filesystem\n\t* but bind only attachs a single filesystem. That means the submount of source directory of mount may disappear in target directory.\n\t* if you want to make submount work, use rbind option.\n\trbind will make entire file hierarchy including submounts mounted on another place.\n\t* NOW, we use bind in container.sh. maybe it need rbind if FS_PREFIX/global/users/$USERNAME/nfs is under glusterfs mountpoint  \n\n* rpc server maybe not security. anyone can call rpc method if he knows ip address.\n    * 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\n xmlrpc.client.ServerProxy can also support https request, it is also a security method\n\t* If we use rpc with authentication, maybe we can use http server and http request to replace rpc\n\n* frontend and backend\n        arch:\n                          +-----------------+\n        Web -- Flask --HttpRest   Core\t    |\n\t                      +-----------------+\n\tNow, HttpRest and Core work as backend\n\tWeb and Flask work as frontend\n\tall modules are in backend\n\tFlask just dispatch urls and render web pages\n\t(Maybe Flask can be merged in Core and works as http server)\n\t(Then Flask needs to render pages, parse urls, response requests, ...)\n\t(It maybe not fine)\n\n* httprest.py :\n\thttphandler needs to call vclustermgr/nodemgr/... to handler request\n\twe need to call these classes in httphandler\n\tWay-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 )\n\tWay-2: use global varibles -- Now this way\n\n* 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 )\n\tso, the environment variables set in shell can not be see in python/...\n\tbut command like below can work :  \n\t\t\tA=ab B=ba ./python.py\n\n* maybe we need to parse argvs in python\n\tsome module to parse argvs : sys.argv, optparse, getopt, argparse\n\n* in shell, { command; } means run command in current shell, \";\" is necessary\n\t( command; ) means run command in sub shell\n\n* function in registered in rpc server must have return.\n\twithout return, the rpc client will raise an exception\n\n*\t** NEED TO BE FIX **\n\twe add a prefix in etcdlib\n\tso when we getkey, key may be a absolute path from base url\n\twhen we setkey use the key we get, etcdlib will append the absolute path to prefix, it will wrong\n\n* overlay : upperdir and workdir must in the same mount filesystem.\n\tthat means we should mount LV first and then mkdir upperdir and workdir in the LV mountpoint\n\n* when use 'worker.py > log' to redirect output of python script, it will empty output of log.\n\tbecause python interpreter will use buffer to collect output.\n\twe can use ways below to fix this problem:\n\t\tstdbuf -o 0 worker.py > log  # but it fail in my try. don't know why\n\t\tpython3 -u worker.py > log # recommended, -u option of python3\n\t\tprint('output', flush=True) # flush option of print\n\t\tsys.stdout.flush() # flush by hand\n\n* CPU QUOTA should not be too small. too small it will work so slowly\n"
  },
  {
    "path": "doc/devdoc/config_info.md",
    "content": "# Info of docklet\n\n## container info\n    container name : username-clusterid-nodeid\n    hostname : host-nodeid  \n    lxc config : /var/lib/lxc/username-clusterid-nodeid/config\n    lxc rootfs : /var/lib/lxc/username-clusterid-nodeid/rootfs\n    lxc rootfs\n          |__ / : aufs : basefs + volume/username-clusterid-nodeid\n\t      |__ /nfs : global/users/username/data\n\t      |__ /etc/hosts : global/users/username/clusters/clusterid/hosts\n\t      |__ /root/.ssh : global/users/username/ssh\n\n\n## ETCD Table\nwe use etcd for some configuration information of our clusters, here is some details.\n\nevery 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.\n\nso, different cluster should has different CLUSTER_NAME.\n\nbelow is content of cluster info in CLUSTER_NAME 'table' in etcd:\n\n    <type>\t\t<name>\t\t<content>\t\t<description>   \n    key    token             random code    token for checking whether master and workers has the same global filesystem\n\n    dir    machines            ...        info of physical clusters\n    dir    machines/allnodes  ip:ok       record all nodes, for recovery and checks  \n    dir    machines/runnodes  ip: ?       record running node for this start up.\n                                      when startup:          ETCD\n\t\t\t\t\t\t\t\t\t                   |   IP:waiting    |   1. worker write worker-ip:waiting\n                   2. master update IP:init-mode       |   IP:init-mode  |   3. worker init itself by init-mode\n\t\t\t\t\t\t\t\t\t                   |   IP:work       |   4. worker finish init and update IP:work\n               5. master add workerip and update IP:ok |   IP:ok         |\n\n    key    service/master   master-ip\n    key    service/mode     new,recovery  start mode of cluster\n\n    key    vcluster/nextid  ID            next available ID\n\n\n\n## filesystem\nhere is the path and content description of docklet filesystem\n\n    FS_PREFIX\n      |__ global/users/{username}\n      |                  |__ clusters/clustername : clusterid, cluster size, status, containers, ...  in json format\n      |                  |__ hosts/id.hosts : ip  host-nodeid  host-nodeid.clustername\n      |                  |__ data : direcroty in distributed filesystem for user to put his data\n      |                  |__ ssh  : ssh keys\n      |\n      |__ local\n            |__ docklet-storage : loop file for lvm\n\t        |__ basefs : base image\n\t        |__ volume / { username-clusterid-nodeid } : upper layer of container\n\n\n\n## vcluster files\n\n### hosts file:(raw)\n    IP-0  host-0  host-0.clustername\n    IP-1  host-1  host-1.clustername\n    ...\n\n### info file:(json)\n    {\n\t    clusterid: ID ,\n\t    status: stopped/running ,\n    \tsize: size ,\n\t    containers: [\n\t    \t{ containername: lxc_name, hostname: hostname, ip: lxc_ip, host: host_ip },\n\t    \t{ containername: lxc_name, hostname: hostname, ip: lxc_ip, host: host_ip },\n\t    \t...\n\t\t\t\t\t\t\t]\n    }  \n"
  },
  {
    "path": "doc/devdoc/network-arch.md",
    "content": "# Architecture of Network\n\n## Architecture of containers networks\nIn current version, to avoid VLAN ID using up, docklet employs a new architecture of containers networks. According to the new architecture, users' networks are exclusive, while the network were shared by all users before. And the new architecture gets rid of VLAN, so it solves the problem of VLAN ID using up. The architecture is shown as follows:\n\n![](./ovs_arch.png)\n\nThere are some points to describe the architecture:\n\n1.Each user has an unique and exclusive virtual network. The container inside the network communicates with outside via gateway.\n\n2.If there is a container in the host, then there will be a user's OVS bridge. Each user's container will connect to user's OVS bridge by Veth Pair. A user's OVS bridge will be named after \"docklet-br-<userid>\".\n\n3.Each user's network is star topology, each host on which there is no gateway will connect to the host on which the user's gateway is by GRE tunnel. Thus, there may be many GRE tunnels between two hosts(Each GRE tunnels belongs to different user.), Docklet takes user's id as keys to distinguish from each other. \n\n4.OVS bridge and GRE tunnels are created and destroyed dynamically, which means that network including bridge and GRE tunnels is created only when user starts the container and is destroyed by calling '/conf/lxc-script/lxc-ifdown' script only when user stops the container.   \n\n5.There are two modes to set up gateways: distributed or centralized. Centralized gateways is the default mode and it will set up the gateways only on Master host, while distributed gateways mode will set up gateways on different workers, just like the picture shown above. NAT/iptables in Linux Kernel is needed when container communicate with outside network via gateway.\n\n## Processing users' requests (Workspace requests)\nThe picture of processing user's requests will show the whole architecture of Docklet. The process is shown as follows, firstly, these are the requests to Workspace: \n\n![](./workspace_requests.png)\n\n## Processing users' requests (Other requests)\nOther requests.\n\n![](./other_requests.png)\n"
  },
  {
    "path": "doc/devdoc/networkmgr.md",
    "content": "# Network Manager\n\n## About\n网络管理是为docklet提供网络管理的模块。\n\n关于需求，主要有两点：\n* 一个中心管理池，按 网络段（IP/CIDR） 给用户分配网络池\n* 很多用户网络池，按 一个或者几个网络地址 给用户的cluster分配网络地址\n\n## Data Structure\n面对这两种需求，设计了两种数据结构来管理网络地址。\n* 区间池 / interval pool ： 分配、回收 网络段\n\n\n    interval pool 中的元素为区间，其由很多个区间组成。\n    一个朴素的 区间池 是这样的 ： interval pool : [A1,A2],[B1,B2],[C1,C2],...[X1,X2]\n    每次申请一段地址的时候，从上述区间中选择一个区间分配，并将该区间中剩余部分放回区间池\n\n    而考虑到 网络段（IP/CIDR） 是 2 的幂的结构，所以可以将区间池进一步设计成如下结构：\n    interval pool:\n            ... ...\n            cidr=16 : [A1,A2], [A3,A4], ...\n            cidr=17 : [B1,B2], [B3,B4], ...\n            cidr=18 : [C1,C2], [C3,C4], ...\n            ... ...\n    上述结构还可以进一步优化，因为 每一个区间的结尾地址可以通过开始地址和CIDR算出来，所以每个区间只需要写一个起始地址就可以了\n    所以：\n    interval pool:\n            ... ...\n            cidr=16 : A1, A3, ...\n            cidr=17 : B1, B3, ...\n            cidr=18 : C1, C3, ...\n            ... ...\n    而其中，每一个元素，比如 A1，其实代表的是一个区间 [A1, A1+2^16-1]\n    这种基于2的幂的区间设计的好处是可以方便的进行 分配 和 合并 区间，操作起来更加高效。\n\n* 枚举池 / enumeration pool : 分配、回收一个、多个网络地址\n\n\n    enum pool 中的元素为单个网络地址，比如：\n    enum pool : A, B, C, D, ... X\n\n## API\n操作上述两种数据结构的API，这里省略\n\n## Network Manager Storage Design\n* center : 中心池，提供 用户网络段 的分配、回收\n\n\n    info : IP/CIDR\n    intervalpool :\n            cidr16 : ...\n            cidr17 : ...\n            ... ...\n\n* system ： 系统保留地址，为系统内部的 网络地址 提供 分配回收\n\n\n    info : IP/CIDR\n    enumpool : ...\n\n* vlan/<username&gt; : 为某个用户提供地址分配、回收服务\n\n\n    info : IP/CIDR\n    enumpool : ...\n    vlanid : id\n"
  },
  {
    "path": "doc/devdoc/openvswitch-vlan.md",
    "content": "# Test of VLAN on openvswitch\n\n## Note 1\n基本操作，建网桥，配置地址，启动网桥\n\n    ovs-vsctl add-br br0\n    ip address add 172.0.0.1/8 dev br0\n    ip link set br0 up\n\n## Note 2\nLXC conf 中指定 pair 的名称，从而方便控制 网络链接\n\n所以，需要修改 conf 文件来实现这一点\n\n    lxc.network.type = veth\n    lxc.network.name = eth0\n    lxc.network.script.up = Bridge=br0 /home/leebaok/Container/lxc-ifup\n    lxc.network.script.down = Bridge=br0 /home/leebaok/Container/lxc-ifdown\n    lxc.network.veth.pair = base\n    lxc.network.ipv4 = 172.0.0.10/8\n    lxc.network.ipv4.gateway = 172.0.0.1\n    lxc.network.flags = up\n    lxc.network.mtu = 1420\n\n我们对上面的配置解释一下：\n* lxc.network.link 现在不需要了\n* lxc.network.script.up/down 来指定container启动前和关闭后的网络准备和释放，这个脚本的路径是物理机的路径，因为这个脚本是由物理机来执行的，“Bridge=br0” 是为了传参数给后面的脚本\n* lxc.network.veth.pair 是网络连接的名字，即container和物理机的哪个口连接\n\n配置了网络设置的脚本路径，我们还需要实现这两个具体的脚本：\n* /home/leebaok/Container/lxc-ifup  \n\n\n    #!/bin/bash\n    # $1 : name of container ( name in lxc-start with -n )\n    # $2 : net\n    # $3 : network flags, up or down\n    # $4 : network type, for example, veth\n    # $5 : value of lxc.network.veth.pair\n    ovs-vsctl --may-exist add-port $Bridge $5\n    # ovs-vsctl set port $5 tag=$Tag\n\n* /home/leebaok/Container/lxc-ifdown\n\n\n    #!/bin/bash\n    # $1 : name of container ( name in lxc-start with -n )\n    # $2 : net\n    # $3 : network flags, up or down\n    # $4 : network type, for example, veth\n    # $5 : value of lxc.network.veth.pair\n    ovs-vsctl --if-exists del-port $Bridge $5\n\n## Note 3\nVLAN tag 操作：\n\n    ovs-vsctl set port <port-name> tag=<tag-id>\n    ovs-vsctl clear port <port-name> tag\n\npatch 是用来连接两个网桥的，操作如下：\n\n    ovs-vsctl add-br br0\n    ovs-vsctl add-br br1\n    ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1\n    ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0\n    # NOW : two bridges are connected by patch\n\n\n## Note 4\n一台机器上一个域的网桥只有一个，比如在 host-0 上，建两个网桥：\n\n    ovs-vsctl add-br br0\n    ip address add 172.0.0.1/8 dev br0\n    ip link set br0 up\n\n    ovs-vsctl add-br br1\n    ip address add 172.0.0.2/8 dev br1\n    ip link set br1 up\n\n则，后配置的那个网桥会失效\n\n因为系统认为，172.0.0.1/8 内的机器都应该在 br0 中\n\n而以下配置是正确的：\n\n    ovs-vsctl add-br br0\n    ip address add 172.0.0.1/24 dev br0\n    ip link set br0 up\n\n    ovs-vsctl add-br br1\n    ip address add 172.0.1.1/24 dev br1\n    ip link set br1 up\n\n## Note 5\n关于网关，网桥/交换机是二层设备，网关是三层组件，我们可以将网桥连接起来，多个网桥共用一个网关\n\n    ovs-vsctl add-br br0\n    ip link set br0 up\n    ovs-vsctl add-br br1\n    ip address add 172.0.0.1/24 dev br1\n    ip link set br1 up\n    ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1\n    ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0\n\n    # lxc config :\n    #   ip -- 172.0.0.11/24\n    #   gateway -- 172.0.0.1\n    #   lxc.network.veth.pair -- base ,  base is connected on br0\n    lxc-start -f container.conf -n base -F -- /bin/bash\n    # NOW ： lxc network is running ok\n\n## Note 6\n基于多个网桥实现VLAN\n\n### 方案一\n\n    ovs-vsctl add-br br0\n    ip link set br0 up\n    ovs-vsctl add-br br1\n    ip address add 172.0.0.1/24 dev br1\n    ip link set br1 up\n    ovs-vsctl add-port br0 patch0 -- set interface patch0 type=patch options:peer=patch1\n    ovs-vsctl add-port br1 patch1 -- set interface patch1 type=patch options:peer=patch0\n\n    # lxc config :\n    #   ip -- 172.0.0.11/24\n    #   gateway -- 172.0.0.1\n    #   lxc.network.veth.pair -- base ,  base is connected on br0\n    lxc-start -f container.conf -n base -F -- /bin/bash\n    # NOW ： lxc network is running ok\n    ## above is the same as before\n\n    ovs-vsctl set port base tag=5\n    ovs-vsctl set port patch0 tag=5\n    # NOW : lxc network is running ok\n\n    #  ARCH                                    \n    +-----------------------+          +----------------------+\n    | br0                   |          | br1 : 172.0.0.1/24   |\n    +--+-----tag=5---tag=5--+          +---+-------+----------+\n       |       |       |       patch       |       |\n       |       |       +-------------------+       |\n       |       |                                   |\n    internal  base:172.0.0.11/24                internal\n              (gateway:172.0.0.1)\n\n    # flow : base --> patch --> br1/internal\n\n* 方案可行\n* 但是，每个 VLAN 需要一个网关\n\n### 方案二 （不可行）\n\n    #  ARCH                                    \n    +-------------------------------------------------------------+\n    | br0                                                         |\n    +--+-----tag=5---tag=5---------+-----tag=6---tag=6---------+--+\n       |       |       |  +-----+  |       |       |  +-----+  |\n       |       |       +--| br1 |--+       |       +--| br2 |--+\n       |       |          +-----+          |          +-----+    \n    internal  base1:172.0.0.11/24         base2:172.0.0.12/24      \n\n    # flow 1 : base1 --> br1 --> internal\n    # flow 2 : base1 --> br1 --> br2 --> base2\n\n* 方案不可行，因为上面的 flow 可以使得 base1、base2 在二层通信，无法隔离\n\n## Note 7\n上述可行方案的简化版\n### 简化版一\n\n    ovs-vsctl add-br br0\n    ip link set br0 up\n    # add a fake bridge connected to br0 with vlan tag=5\n    ovs-vsctl add-br fakebr br0 5\n    ip address add 172.0.0.1/24 dev fakebr\n    ip link set fakebr up\n\n    # lxc config:\n    #   ip : 172.0.0.11/24\n    #   gateway : 172.0.0.1/24\n    #   lxc.network.veth.pair -- base ,  base is connected on br0\n    lxc-start -f container.conf -n base -F -- /bin/bash\n\n    ovs-vsctl set port base tag=5\n\n    #  ARCH                                    \n    +-----------------------+          \n    | br0                   |    \n    +--+-----tag=5---tag=5--+      \n       |       |       |\n       |       |     fakebr:172.0.0.1/24\n       |       |                               \n    internal  base:172.0.0.11/24             \n              (gateway:172.0.0.1)\n\n    # flow : base --> fakebr\n\n### 简化版二\n\n    ovs-vsctl add-br br0\n    ip link set br0 up\n    # add an internal interface for vlan\n    ovs-vsctl add-port br0 vlanif tag=5 -- set interface vlanif type=internal\n    ip address add 172.0.0.1/24 dev vlanif\n    ip link set vlanif up\n\n    # lxc config:\n    #   ip : 172.0.0.11/24\n    #   gateway : 172.0.0.1/24\n    #   lxc.network.veth.pair -- base ,  base is connected on br0\n    lxc-start -f container.conf -n base -F -- /bin/bash\n\n    ovs-vsctl set port base tag=5\n\n    #  ARCH                                    \n    +-----------------------+          \n    | br0                   |    \n    +--+-----tag=5---tag=5--+      \n       |       |       |\n       |       |     vlanif:172.0.0.1/24\n       |       |                               \n    internal  base:172.0.0.11/24             \n              (gateway:172.0.0.1)\n\n    # flow : base --> vlanif\n\n### 简化版一 & 简化版二\n使用 ovs-vsctl show 查看的时候，上述两个版本显示的信息是一样的，说明 fakebr 其实本质上可能就是一个 internal interface\n\n其实，方案一中，对 br1 的 IP（172.0.0.1/24）的配置，其实就是对 br1 的 internal 的 interface 的配置，所以其实多余的网桥不是必须的，而 interface 才是真正需要的。\n\n而，internal interface 相当于是连接着本地Linux的虚拟网卡，这块网卡的另一端连着OVS的虚拟网桥。\n\n而，Linux 的网络栈又管理着物理网卡、虚拟网卡，以及对这些网卡的包进行转发、路由等处理。\n\n似乎，Linux 的网络栈又成了一个大的交换机/网桥，上面连接着 internal interface 和 物理网卡。\n\n## Note 8\n基于上述的实践和探索，其实 **我们需要给一个VLAN配置一个可以出去的网关、网卡。**\n\n那么，我们一个简单可行的方案可以这样：\n\n    +------------------------------------------------------------------------------+\n    | bridge                                                                       |\n    |        <------- VLAN ID=5 --------->              <---- VLAN ID=6 ------>    |\n    +--+-----tag=5---tag=5------------tag=5-------------tag=6-------------tag=6----+\n       |       |       |                |                 |                 |\n       |       |  lxc-2:172.0.0.12/24   |                 |                 |\n    internal   |  (gateway:172.0.0.1)   |                 |                 |\n               |                        |                 |                 |\n        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\n        (gateway:172.0.0.1)       internal           (gateway:172.0.1.1)  internal\n                                        |                                   |\n                                        |                                   |\n                                        +----------- NAT / iptables --------+\n                                                          ||||\n                                                          ||||\n                                                         \\\\\\///\n                                                          \\\\//\n                                                           \\/\n\n\n\n\n# end\n"
  },
  {
    "path": "doc/devdoc/proxy-control.md",
    "content": "# Some Note for configurable-http-proxy usage\n\n## intsall\n    sudo apt-get install nodejs nodejs-legacy npm\n    sudo npm install -g configurable-http-proxy\n\n## start\n    configurable-http-proxy -h  : for help\n    configurable-http-proxy --ip IP \\\n\t\t\t\t\t    \t--port PORT \\\n\t\t\t\t\t     \t--api-ip IP \\\n\t\t\t\t\t    \t--api-port PORT \\\n\t\t\t\t\t    \t--default-target http://IP:PORT \\\n\t\t\t\t\t    \t--log-level debug/info/warn/error\ndefault ip:port is 0.0.0.0:8000,\ndefault api-ip:api-port is localhost:8001\n\n## control route table\n### get route table\n* without token:\n    \tcurl http://localhost:8001/api/routes\n* with token:\n     \tcurl -H \"Authorization: token TOKEN\" http://localhost:8001/api/routes\n### add/set route table\n* without token:\n    \tcurl -XPOST --data '{\"target\":\"http://TARGET-IP:TARGET-PORT\"}' http://localhost:8001/api/routes/PROXY-URL\n* with token:\n    \tcurl -H \"Authorization: token TOKEN\" -XPOST --data '{\"target\":\"http://TARGET-IP:TARGET-PORT\"}' http://localhost:8001/api/routes/PROXY-URL\n### delete route table line\n* without token:\n    \tcurl -XDELETE http://localhost:8001/api/routes/PROXY-URL\n* with token:\n    \tcurl -H \"Authorization: token TOKEN\" -XDELETE http://localhost:8001/api/routes/PROXY-URL\n"
  },
  {
    "path": "doc/devdoc/startup.md",
    "content": "# startup mode\n\n## new mode\n#### step 1 : data\n           <Master>\n    clean etcd table\n    write token\n    init etcd table\n    clean global directory of user clusters\n#### step 2 : nodemgr\n           <Master>                                         <Slave>\n    init network                                 \n    wait for all nodes starts            \n         |_____ listen node joins     IP:waiting  <---    worker starts\n\t            update etcd   ---->  IP:init-mode --->   worker init\n\t\t\t                                                  |____ stop all containers\n\t\t\t\t    \t\t\t\t\t\t\t\t\t\t  |____ umount mountpoint, delete lxc files, delete LV\n\t\t\t\t\t    \t\t\t\t\t\t\t\t\t  |____ delete VG, umount loop dev, delete loop file\n\t\t\t\t\t\t    \t\t\t\t\t\t\t\t  |____ init loop file, loop dev, create VG\n    \t\t\tadd node to list <--- IP:work      <----  init done, begin work\n    check all nodes begin work\n#### step 3 : vclustermgr\n    Nothing to do\n\n\n\n\n## recovery mode\n#### step 1 : data\n           <Master>\n    write token\n    init some of etcd table\n#### step 2 : nodemgr\n           <Master>                                         <Slave>\n    init network                                 \n    wait for all nodes starts            \n          |_____ listen node joins     IP:waiting  <---    worker starts\n\t             update etcd   ---->  IP:init-mode --->   worker init\n\t\t\t                                                  |____ check loop file, loop dev, VG\n\t\t\t\t    \t\t\t\t\t\t\t\t\t\t  |____ check all containers and mountpoint\n\t\t\t     add node to list <--- IP:work      <----  init done, begin work\n    check all nodes begin work\n#### step 3 : vclustermgr\n           <Master>                                        <Slave>\n    recover vclusters:some need start  --------------->   recover containers: some need start\n"
  },
  {
    "path": "doc/devguide/devguide.md",
    "content": "# Docklet Development Guide on GitHub\nThis document is intended for GitHubers to contribute for Docklet System.\n\n## Introduction of Docklet Development Workflow\nWe use fork and pull request workflow to push forward Docklet Project.\n\n![Docklet Workflow](images/workflow.png)\n\n## Step by Step\n### Prepare\nBefore work, we need to prepare our working repository. These actions should be executed just once.\n##### Step 1 : fork\nOpen https://github.com/unias/docklet in your browser and click **Fork** button on the top-right corner.\n##### Step 2 : clone & config\n* clone docklet from your github repository\n```\ngit clone https://github.com/YourName/docklet.git\n```\n* config your local repository\n```\n# add unias/docklet as your upstream\ngit remote add upstream https://github.com/unias/docklet.git\n# set push to upstream not work\ngit remote set-url --push upstream no_push\n```\n\n### Work\nThis part is about the steps of making contributions to Docklet by pull request.\n#### Work : Begin\n##### Step 3 : fetch\nFetch the latest code from **upstream(unias/docklet)**\n```\ngit fetch upstream\n```\n##### Step 4 : branch\nCreate new branch for your work\n```\ngit checkout -b BranchName upstream/master\n```\nThis is not the step you must do and you can work on local master branch. But we recommend you follow these steps. Using branch to develop new features fits git.\n#### Work : Work\nNow you can focus on your work by **commit** and **push**.\n##### Step 5 : commit & commit\nCommit is commit. Nothing to say.\n##### Step 6 : push & push\nPush your work to **your own Github repository** by **BranchName**\n```\ngit push origin BranchName\n```\n#### Work : End\nAfter you complete work of this feature, you maybe want to create a pull request to unias/docklet. Please follow steps below.\n##### Step 7 : fetch\nFetch the latest code from **unias/docklet**\n```\ngit fetch upstream\n```\n##### Step 8 : merge\nMerge upstream's latest code to your working branch\n```\ngit merge upstream/master\n```\nPlease ensure that you are on your working branch.\n\nIf conflict happens, resolve it and commit.\n##### Step 9 : push\nPush to your github repository by BranchName.\n```\ngit push origin BranchName\n```\n##### Step 10 : pull request\nOpen https://github.com/YourName/docklet, click **New pull request** and select your working **BranchName** to create the pull request.\n\n## Tips\n##### local master\nAfter you fetch upstream code, you can move forward your local master branch to upstream/master. And push your github repository master branch to update.\n```\ngit fetch upstream\ngit checkout master\ngit merge upstream/master\ngit push origin master\n```\n##### pretty git log or git log with GUI\nYou can config your git log command with pretty format.\n```\ngit config --global alias.lg \"log --graph --color --pretty=format:' %Cred%h %Creset/ %<(10,trunc)%Cblue%an%Creset | %<(60,trunc)%s | %cr %Cred%d' --remotes --branches\"\n```\nNow, type **git lg** to see what happens.\n\nOf course, you can use GUI with git. **gitg** is a good choice. It shows log of git very friendly.\n##### understand git log\ngit log has much information. You should understand the log info of git. This can help you know how to move forward your work. Especially the reference of branches : upstream/master, HEAD, master, origin/master, other branches.\n##### graphs/network of github\nThe Graphs/Network of Github is very useful. With this, you can know whether you can create a pull request without conflict. Open https://github.com/unias/docklet/network in your browser and see the network graph of docklet.  \n"
  },
  {
    "path": "doc/example/example-LogisticRegression.py",
    "content": "# import package\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom sklearn import linear_model, datasets\n%matplotlib inline\n\n# load data : we only use target==0 and target==1 (2 types classify) and feature 0 and feature 2 ()\niris = datasets.load_iris()\nX = iris.data[iris.target!=2][:, [0,2]]  \nY = iris.target[iris.target!=2]\n\nh = .02  # step size in the mesh\n\nlogreg = linear_model.LogisticRegression(C=1e5)\nlogreg.fit(X, Y)\n\n# Plot the decision boundary. For that, we will assign a color to each\n# point in the mesh [x_min, m_max]x[y_min, y_max].\nx_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5\ny_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5\nxx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))\nZ = logreg.predict(np.c_[xx.ravel(), yy.ravel()])\n\n# Put the result into a color plot\nZ = Z.reshape(xx.shape)\n#plt.figure(1, figsize=(4, 3))\nplt.pcolormesh(xx, yy, Z, cmap=plt.cm.Paired)\nplt.xlabel('Sepal length')\nplt.ylabel('Sepal width')\n\n# Plot also the training points\nplt.scatter(X[:, 0], X[:, 1], c=Y, edgecolors='k', cmap=plt.cm.Paired)\nplt.xlabel('Sepal length')\nplt.ylabel('Sepal width')\n\nplt.xlim(xx.min(), xx.max())\nplt.ylim(yy.min(), yy.max())\nplt.xticks(())\nplt.yticks(())\n\n"
  },
  {
    "path": "meter/connector/master.py",
    "content": "#!/usr/bin/python3\n\nimport socket, select, errno, threading, os\n\nclass master_connector:\n\t\n\ttcp_port = 1727\n\tmax_minions = 256\n\t\n\tconn = {}\n\tepoll_fd = select.epoll()\n\t\n\tdef establish_vswitch(ovsname):\n\t\tos.system('ovs-vsctl del-br ovs-%s >/dev/null 2>&1' % ovsname)\n\t\tos.system('ovs-vsctl add-br ovs-%s' % ovsname)\n\t\tos.system('brctl addif ovs-bridge ovs-%s >/dev/null 2>&1' % ovsname)\n\t\tos.system('ip link set ovs-system up')\n\t\tos.system('ip link set ovs-%s up' % ovsname)\n\t\n\tdef build_gre_conn(ovsname, ipaddr):\n\t\tname = ipaddr.replace('.','_')\n\t\tos.system('ovs-vsctl add-port ovs-%s gre-%s -- set interface gre-%s type=gre options:remote_ip=%s 2>/dev/null' % (ovsname, name, name, ipaddr))\n\t\n\tdef break_gre_conn(ovsname, ipaddr):\n\t\tname = ipaddr.replace('.','_')\n\t\tos.system('ovs-vsctl del-port ovs-%s gre-%s 2>/dev/null' % (ovsname, name))\n\t\n\tdef close_connection(fd):\n\t\tmaster_connector.epoll_fd.unregister(fd)\n\t\tmaster_connector.conn[fd][0].close()\n\t\taddr = master_connector.conn[fd][1]\n\t\tmaster_connector.conn.pop(fd)\n\t\tmaster_connector.break_gre_conn('master', addr)\n\n\tdef do_message_response(input_buffer):\n\t\tassert(input_buffer == b'ack')\n\t\treturn b'ack'\n\t\n\tdef start():\n\t\tthread = threading.Thread(target = master_connector.run_forever, args = [])\n\t\tthread.setDaemon(True)\n\t\tthread.start()\n\t\treturn thread\n\t\n\tdef run_forever():\n\t\tlisten_fd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)\n\t\tlisten_fd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n\t\tlisten_fd.bind(('', master_connector.tcp_port))\n\t\tlisten_fd.listen(master_connector.max_minions)\n\t\t\n\t\tmaster_connector.epoll_fd.register(listen_fd.fileno(), select.EPOLLIN)\n\t\t\n\t\tdatalist = {}\n\t\t\n\t\tmaster_connector.establish_vswitch('master')\n\t\ttry:\n\t\t\twhile True:\n\t\t\t\tepoll_list = master_connector.epoll_fd.poll()\n\t\t\t\tfor fd, events in epoll_list:\n\t\t\t\t\tif fd == listen_fd.fileno():\n\t\t\t\t\t\tfileno, addr = listen_fd.accept()\n\t\t\t\t\t\tfileno.setblocking(0)\n\t\t\t\t\t\tmaster_connector.epoll_fd.register(fileno.fileno(), select.EPOLLIN | select.EPOLLET)\n\t\t\t\t\t\tmaster_connector.conn[fileno.fileno()] = (fileno, addr[0])\n\t\t\t\t\t\tmaster_connector.build_gre_conn('master', addr[0])\n\t\t\t\t\telif select.EPOLLIN & events:\n\t\t\t\t\t\tdatas = b''\n\t\t\t\t\t\twhile True:\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\tdata = master_connector.conn[fd][0].recv(10)\n\t\t\t\t\t\t\t\tif not data and not datas:\n\t\t\t\t\t\t\t\t\tmaster_connector.close_connection(fd)\n\t\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t\tdatas += data\n\t\t\t\t\t\t\texcept socket.error as msg:\n\t\t\t\t\t\t\t\tif msg.errno == errno.EAGAIN:\n\t\t\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\t\t\tdatalist[fd] = master_connector.do_message_response(datas)\n\t\t\t\t\t\t\t\t\t\tmaster_connector.epoll_fd.modify(fd, select.EPOLLET | select.EPOLLOUT)\n\t\t\t\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\t\t\t\tmaster_connector.close_connection(fd)\n\t\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t\tmaster_connector.close_connection(fd)\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\telif select.EPOLLOUT & events:\n\t\t\t\t\t\tsendLen = 0\n\t\t\t\t\t\twhile True:\n\t\t\t\t\t\t\tsendLen += master_connector.conn[fd][0].send(datalist[fd][sendLen:])\n\t\t\t\t\t\t\tif sendLen == len(datalist[fd]):\n\t\t\t\t\t\t\t\tbreak\n\t\t\t\t\t\tmaster_connector.epoll_fd.modify(fd, select.EPOLLIN | select.EPOLLET)\n\t\t\t\t\telif select.EPOLLHUP & events:\n\t\t\t\t\t\tmaster_connector.close_connection(fd)\n\t\t\t\t\telse:\n\t\t\t\t\t\tcontinue\n\t\tfinally:\n\t\t\tos.system('ovs-vsctl del-br ovs-master >/dev/null 2>&1')\n\t\t\n"
  },
  {
    "path": "meter/connector/minion.py",
    "content": "#!/usr/bin/python3\n\nimport socket, time, threading, os\n\nclass minion_connector:\n\t\n\tdef connect(server_ip):\n\t\tfrom connector.master import master_connector\n\t\tconnected = True\n\t\twhile True:\n\t\t\ttry:\n\t\t\t\tfd = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)\n\t\t\t\tfd.connect((server_ip, master_connector.tcp_port))\n\t\t\t\tconnected = True\n\t\t\t\tprint(\"[info]\", \"connected to master.\")\n\t\t\t\t\n\t\t\t\tmaster_connector.establish_vswitch('minion')\n\t\t\t\tmaster_connector.build_gre_conn('minion', server_ip)\n\t\t\t\t\n\t\t\t\twhile True:\n\t\t\t\t\tdata = b'ack'\n\t\t\t\t\tif fd.send(data) != len(data):\n\t\t\t\t\t\tbreak\n\t\t\t\t\treadData = fd.recv(1024)\n\t\t\t\t\ttime.sleep(0.5)\n\t\t\t\tfd.close()\n\t\t\texcept socket.error as e:\n\t\t\t\tmaster_connector.break_gre_conn('minion', server_ip)\n\t\t\t\tif connected:\n\t\t\t\t\tprint(\"[info]\", \"non-connected with master.\")\n\t\t\texcept Exception as e:\n\t\t\t\tpass\n\t\t\tfinally:\n\t\t\t\tif connected:\n\t\t\t\t\tos.system('ovs-vsctl del-br ovs-minion >/dev/null 2>&1')\n\t\t\t\tconnected = False\n\t\t\t\ttime.sleep(1)\n\t\n\tdef start(server_ip):\n\t\tthread = threading.Thread(target = minion_connector.connect, args = [server_ip])\n\t\tthread.setDaemon(True)\n\t\tthread.start()\n\t\treturn thread\n"
  },
  {
    "path": "meter/daemon/http.py",
    "content": "import json, cgi, threading\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\n\nclass base_http_handler(BaseHTTPRequestHandler):\n\t\n\tdef load_module(self):\n\t\treturn None\n\t\n\tdef do_POST(self):\n\t\ttry:\n\t\t\tdefault_exception = 'unsupported request.'\n\t\t\tsuccess = True\n\t\t\tdata = None\n\t\t\t\n\t\t\tlength = self.headers['content-length']\n\t\t\tif length == None:\n\t\t\t\tlength = self.headers['content-length'] = 0\n\t\t\tif int(length) > (1<<12):\n\t\t\t\traise Exception(\"data too large\")\n\t\t\thttp_form = cgi.FieldStorage(fp=self.rfile, headers=self.headers,environ={'REQUEST_METHOD':'POST','CONTENT_TYPE': \"text/html\"})\n\t\t\t\n\t\t\tform = {}\n\t\t\tfor item in http_form:\n\t\t\t\ttry:\n\t\t\t\t\tvalue = http_form[item].file.read().strip()\n\t\t\t\texcept:\n\t\t\t\t\tvalue = http_form[item].value\n\t\t\t\ttry:\n\t\t\t\t\tvalue = value.decode()\n\t\t\t\texcept:\n\t\t\t\t\tpass\n\t\t\t\tform[item] = value\n\t\t\t\n\t\t\tparts = self.path.split('/', 2)\n\t\t\tif len(parts) != 3:\n\t\t\t\traise Exception(default_exception)\n\t\t\t[null, version, path] = parts\n\t\t\t\n\t\t\tpymodule = self.load_module() + '_' + version\n\t\t\tmodule = __import__('daemon.' + pymodule)\n\t\t\thandler = module.__dict__[pymodule].__dict__['case_handler']\n\t\t\tmethod = path.replace('/', '_')\n\t\t\tif not hasattr(handler, method):\n\t\t\t\traise Exception(default_exception)\n\t\t\t\n\t\t\tdata = handler.__dict__[method](form, self.handler_class.args)\n\t\texcept Exception as e:\n\t\t\tsuccess = False\n\t\t\tdata = {\"reason\": str(e)}\n\t\tfinally:\n\t\t\tself.send_response(200)\n\t\t\tself.send_header(\"Content-type\", \"application/json\")\n\t\t\tself.end_headers()\n\t\t\tself.wfile.write(json.dumps({\"success\": success, \"data\": data}).encode())\n\t\t\tself.wfile.write(\"\\n\".encode())\n\t\treturn\n\nclass master_http_handler(base_http_handler):\n\t\n\thttp_port = 1728\n\t\n\tdef load_module(self):\n\t\tself.handler_class = master_http_handler\n\t\treturn 'master'\n\nclass minion_http_handler(base_http_handler):\n\t\n\thttp_port = 1729\n\t\n\tdef load_module(self):\n\t\tself.handler_class = minion_http_handler\n\t\treturn 'minion'\n\nclass http_daemon_listener:\n\t\n\tdef __init__(self, handler_class, args = None):\n\t\thandler_class.args = args\n\t\tself.handler_class = handler_class\n\n\tdef listen(self):\n\t\tserver = HTTPServer(('', self.handler_class.http_port), self.handler_class)\n\t\tserver.serve_forever()\n"
  },
  {
    "path": "meter/daemon/master_v1.py",
    "content": "import subprocess, os\n\ndef http_client_post(ip, port, url, entries = {}):\n\timport urllib.request, urllib.parse, json\n\turl = url if not url.startswith('/') else url[1:]\n\tresponse = urllib.request.urlopen('http://%s:%d/%s' % (ip, port, url), urllib.parse.urlencode(entries).encode())\n\tobj = json.loads(response.read().decode().strip())\n\tresponse.close()\n\treturn obj\n\nclass case_handler:\n\t# [Order-by] lexicographic order\n\t\n\t# curl -L -X POST http://0.0.0.0:1728/v1/minions/list\n\tdef minions_list(form, args):\n\t\tminions = []\n\t\tfor item in args.conn:\n\t\t\tminions.append(args.conn[item][1])\n\t\treturn {\"minions\": minions}\n\t\n\t# curl -L -X POST -F mem=4096 -F cpu=2 http://0.0.0.0:1728/v1/resource/allocation\n\tdef resource_allocation(form, args):\n\t\tmem = int(form['mem'])\n\t\tcpu = int(form['cpu'])\n\t\tcandidates = {}\n\t\tfrom daemon.http import minion_http_handler\n\t\tfor item in args.conn:\n\t\t\taddr = args.conn[item][1]\n\t\t\tobj = http_client_post(addr, minion_http_handler.http_port, '/v1/system/memsw/available')\n\t\t\tif obj['success'] and obj['data']['Mbytes'] >= mem:\n\t\t\t\tcandidates[addr] = obj['data']\n\n\t\tif len(candidates) <= 0:\n\t\t\traise Exception(\"no minions\")\n\t\telse:\n\t\t\tfrom policy.allocate import candidates_selector\n\t\t\tone = candidates_selector.select(candidates)\n\t\t\treturn {\"recommend\": one}\n\t\n\t# curl -L -X POST -F user=docklet http://0.0.0.0:1728/v1/user/live/add\n\tdef user_live_add(form, args):\n\t\tif not os.path.exists('/var/lib/docklet/global/users/%s' % form['user']):\n\t\t\treturn False\n\t\tsubprocess.getoutput('echo live > /var/lib/docklet/global/users/%s/status' % form['user'])\n\t\treturn True\n\t\n\t# curl -L -X POST -F user=docklet http://0.0.0.0:1728/v1/user/live/remove\n\tdef user_live_remove(form, args):\n\t\tsubprocess.getoutput('rm -f /var/lib/docklet/global/users/%s/status' % form['user'])\n\t\treturn True\n\t\n\t# curl -L -X POST http://0.0.0.0:1728/v1/user/live/list\n\tdef user_live_list(form, args):\n\t\treturn subprocess.getoutput('ls -1 /var/lib/docklet/global/users/*/status 2>/dev/null | awk -F\\/ \\'{print $(NF-1)\\'}').split()\n\t\n\t\n\t\n\t\n"
  },
  {
    "path": "meter/daemon/minion_v1.py",
    "content": "from intra.system import system_manager\nfrom intra.billing import billing_manager\nfrom intra.cgroup import cgroup_manager\nfrom policy.quota import *\nfrom intra.smart import smart_controller\n\nclass case_handler:\n\t# [Order-by] lexicographic order\n\t\n\t# curl -L -X POST -F uuid=docklet-1-0 http://0.0.0.0:1729/v1/billing/increment\n\tdef billing_increment(form, args):\n\t\treturn billing_manager.fetch_increment_and_clean(form['uuid'])\n\t\n\t# curl -L -X POST http://0.0.0.0:1729/v1/cgroup/container/list\n\tdef cgroup_container_list(form, args):\n\t\treturn cgroup_manager.get_cgroup_containers()\n\t\n\t# curl -L -X POST -F policy=etime_rev_policy http://0.0.0.0:1729/v1/smart/quota/policy\n\tdef smart_quota_policy(form, args):\n\t\tmsg = 'success'\n\t\ttry:\n\t\t\tsmart_controller.set_policy(eval(form['policy']))\n\t\texcept Exception as e:\n\t\t\tmsg = e\n\t\treturn {'message': msg}\n\t\n\t# curl -L -X POST -F uuid=n1 http://0.0.0.0:1729/v1/cgroup/container/limit\n\tdef cgroup_container_limit(form, args):\n\t\treturn cgroup_manager.get_container_limit(form['uuid'])\n\t\n\t# curl -L -X POST -F uuid=n1 http://0.0.0.0:1729/v1/cgroup/container/sample\n\tdef cgroup_container_sample(form, args):\n\t\treturn cgroup_manager.get_container_sample(form['uuid'])\n\t\n\t# curl -L -X POST http://0.0.0.0:1729/v1/system/loads\n\tdef system_loads(form, args):\n\t\treturn system_manager.get_system_loads()\n\t\n\t# curl -L -X POST http://0.0.0.0:1729/v1/system/memsw/available\n\tdef system_memsw_available(form, args):\n\t\treturn system_manager.get_available_memsw()\n\t\n\t# curl -L -X POST -F size=16 http://0.0.0.0:1729/v1/system/swap/extend\n\tdef system_swap_extend(form, args):\n\t\treturn system_manager.extend_swap(int(form['size']))\n\t\n\t# curl -L -X POST http://0.0.0.0:1729/v1/system/swap/clear\n\tdef system_swap_clear(form, args):\n\t\treturn system_manager.clear_all_swaps()\n\t\n\t# curl -L -X POST http://0.0.0.0:1729/v1/system/total/physical/memory\n\tdef system_total_physical_memory(form, args):\n\t\treturn system_manager.get_total_physical_memory_for_containers()\n\n\t'''\n\t# curl -X POST -F uuid=n1 http://0.0.0.0:1729/v1/blacklist/add\n\tdef blacklist_add(form):\n\t\texists = form['uuid'] in smart_controller.blacklist\n\t\tif not exists:\n\t\t\tsmart_controller.blacklist.add(form['uuid'])\n\t\treturn {\"changed\": not exists}\n\t\n\t# curl -X POST -F uuid=n1 http://0.0.0.0:1729/v1/blacklist/remove\n\tdef blacklist_remove(form):\n\t\texists = form['uuid'] in smart_controller.blacklist\n\t\tif exists:\n\t\t\tsmart_controller.blacklist.remove(form['uuid'])\n\t\treturn {\"changed\": exists}\n\t\n\t# curl -X POST http://0.0.0.0:1729/v1/blacklist/show\n\tdef blacklist_show(form):\n\t\tblacklist = []\n\t\tfor item in smart_controller.blacklist:\n\t\t\tblacklist.append(item)\n\t\treturn blacklist\n\t'''\n"
  },
  {
    "path": "meter/intra/billing.py",
    "content": "import subprocess, time, os\n\nfrom intra.system import system_manager\n\nclass billing_manager:\n\t\n\thistory_book = {}\n\t\n\tdef on_lxc_acct_usage(uuid, prev, curr, interval):\n\t\tcpu_gen = max(0, curr['cpu_sample'] - prev['cpu_sample']) >> 20 # in ms\n\t\tmem_gen = ((curr['mem_phys_sample'] + prev['mem_phys_sample']) * interval) >> 11 # in kbytes\n\t\ttry:\n\t\t\tos.makedirs('%s/%s' % (system_manager.db_prefix, uuid))\n\t\texcept:\n\t\t\tpass\n\t\twith open('%s/%s/usage' % (system_manager.db_prefix, uuid), 'a') as fp:\n\t\t\tfp.write('%d %d\\n' % (cpu_gen, mem_gen))\n\t\n\tdef add_usage_sample(uuid, sample, interval):\n\t\tif uuid in billing_manager.history_book:\n\t\t\tbilling_manager.on_lxc_acct_usage(uuid, billing_manager.history_book[uuid], sample, interval)\n\t\tbilling_manager.history_book[uuid] = sample\n\t\n\tdef clean_dead_node(uuid):\n\t\tif uuid in billing_manager.history_book:\n\t\t\tbilling_manager.history_book.pop(uuid)\n\t\n\tdef fetch_increment_and_clean(uuid):\n\t\tcpu_acct = 0.0\n\t\tmem_acct = 0.0\n\t\tcnt_acct = 0\n\t\ttry:\n\t\t\tfetch_path = '%s/%s/%f' % (system_manager.db_prefix, uuid, time.time())\n\t\t\tos.rename('%s/%s/usage' % (system_manager.db_prefix, uuid), fetch_path)\n\t\t\twith open(fetch_path, 'r') as fp:\n\t\t\t\tline = fp.readline()\n\t\t\t\twhile line != '':\n\t\t\t\t\t[cpu, mem] = line.split()\n\t\t\t\t\tline = fp.readline()\n\t\t\t\t\tcnt_acct += 1\n\t\t\t\t\tcpu_acct += float(cpu)\n\t\t\t\t\tmem_acct += float(mem)\n\t\t\tos.remove(fetch_path)\n\t\texcept:\n\t\t\tpass\n\t\treturn {\"cpu_acct\": cpu_acct, \"mem_acct\": mem_acct, \"cnt_acct\": cnt_acct}\n"
  },
  {
    "path": "meter/intra/cgroup.py",
    "content": "import subprocess, os\n\nclass cgroup_controller:\n\t\n\tdef read_value(group, uuid, item):\n\t\tpath = cgroup_manager.__default_prefix__ % (group, uuid, item)\n\t\tif not os.path.exists(path):\n\t\t\traise Exception('read: container \"%s\" not found!' % uuid)\n\t\twith open(path, 'r') as file:\n\t\t\tvalue = file.read()\n\t\treturn value.strip()\n\t\n\tdef write_value(group, uuid, item, value):\n\t\tpath = cgroup_manager.__default_prefix__ % (group, uuid, item)\n\t\tif not os.path.exists(path):\n\t\t\traise Exception('write: container \"%s\" not found!' % uuid)\n\t\ttry:\n\t\t\twith open(path, 'w') as file:\n\t\t\t\tfile.write(str(value))\n\t\texcept:\n\t\t\tpass\n\nclass cgroup_manager:\n\t\n\t__prefix_docker__ = '/sys/fs/cgroup/%s/system.slice/docker-%s.scope/%s'\n\t__prefix_lxc__ = '/sys/fs/cgroup/%s/lxc/%s/%s'\n\t__prefix_lxcinit__ = '/sys/fs/cgroup/%s/init.scope/lxc/%s/%s'\n\t\n\tdef set_default_memory_limit(limit):\n\t\tcgroup_manager.__default_memory_limit__ = limit\n\t\n\tdef set_cgroup_prefix(prefix = __prefix_lxc__):\n\t\tcgroup_manager.__default_prefix__ = prefix\n\t\n\tdef auto_detect_prefix():\n\t\tcgroup_manager.__default_prefix__ = cgroup_manager.__prefix_docker__\n\t\tif len(cgroup_manager.get_cgroup_containers()) > 0:\n\t\t\treturn\n\t\tcgroup_manager.__default_prefix__ = cgroup_manager.__prefix_lxcinit__\n\t\tif len(cgroup_manager.get_cgroup_containers()) > 0:\n\t\t\treturn\n\t\tcgroup_manager.__default_prefix__ = cgroup_manager.__prefix_lxc__\n\t\tif len(cgroup_manager.get_cgroup_containers()) > 0:\n\t\t\treturn\n\t\t# print(\"[info]\", \"set cgroup prefix to %s\" % cgroup_manager.__default_prefix__)\n\t\n\tdef get_cgroup_containers():\n\t\tcontainers = subprocess.getoutput(\"find %s -type d 2>/dev/null | awk -F\\/ '{print $(NF-1)}'\" % (cgroup_manager.__default_prefix__ % ('cpu', '*', '.'))).split()\n\t\tuuids = []\n\t\tfor item in containers:\n\t\t\tif item.startswith('docker-') and item.endswith('.scope') and len(item) > 64:\n\t\t\t\tuuids.append(item[7:-6])\n\t\t\telse:\n\t\t\t\tuuids.append(item)\n\t\treturn uuids\n\t\n\tdef get_container_pid(uuid):\n\t\treturn int(cgroup_controller.read_value('cpu', uuid, 'tasks').split()[0])\n\t\n\tdef get_container_sample(uuid):\n\t\tmem_page_sample = int(cgroup_controller.read_value('memory', uuid, 'memory.memsw.usage_in_bytes'))\n\t\tmem_phys_sample = int(cgroup_controller.read_value('memory', uuid, 'memory.usage_in_bytes'))\n\t\tcpu_sample = int(cgroup_controller.read_value('cpu', uuid, 'cpuacct.usage'))\n\t\tpids_sample = int(cgroup_controller.read_value('pids', uuid, 'pids.current'))\n\t\tcontainer_pid = cgroup_manager.get_container_pid(uuid)\n\t\t\n\t\tfrom intra.system import system_manager\n\t\treal_time = system_manager.get_proc_etime(container_pid)\n\t\treturn {\"cpu_sample\": cpu_sample, \"pids_sample\": pids_sample, \"mem_page_sample\": mem_page_sample, \"mem_phys_sample\": mem_phys_sample, \"pid\": container_pid, \"real_time\": real_time}\n\t\n\tdef get_container_limit(uuid):\n\t\tmem_phys_quota = int(cgroup_controller.read_value('memory', uuid, 'memory.limit_in_bytes'))\n\t\tmem_page_quota = int(cgroup_controller.read_value('memory', uuid, 'memory.memsw.limit_in_bytes'))\n\t\tcpu_shares = int(cgroup_controller.read_value('cpu', uuid, 'cpu.shares'))\n\t\tcpu_quota = int(cgroup_controller.read_value('cpu', uuid, 'cpu.cfs_quota_us'))\n\t\tcpu_quota = cpu_quota if cpu_quota >= 0 else -1\n\t\t\n\t\tpids_quota = cgroup_controller.read_value('pids', uuid, 'pids.max')\n\t\tpids_quota = int(pids_quota) if pids_quota != 'max' else -1\n\t\treturn {\"cpu_quota\": cpu_quota, \"cpu_shares\": cpu_shares, \"mem_phy_quota\": mem_phys_quota, \"mem_page_quota\": mem_page_quota, \"pids_quota\": pids_quota}\n\n\tdef get_container_oom_status(uuid):\n\t\t[_x, idle, _y, oom] = cgroup_controller.read_value('memory', uuid, 'memory.oom_control').split()\n\t\treturn (idle == '1', oom == '1')\n\n\tdef set_container_oom_idle(uuid, idle):\n\t\tcgroup_controller.write_value('memory', uuid, 'memory.oom_control', 1 if idle else 0)\n\t\n\tdef protect_container_oom(uuid):\n\t\tcgroup_controller.write_value('memory', uuid, 'memory.oom_control', 1)\n\t\tdata = cgroup_manager.get_container_limit(uuid)\n\t\tif data[\"mem_page_quota\"] >= 9223372036854771712:\n\t\t\tmemory_limit_in_bytes = cgroup_manager.__default_memory_limit__ << 30\n\t\t\tmem_phy_quota = min(data[\"mem_phy_quota\"], memory_limit_in_bytes)\n\t\t\tmem_page_quota = memory_limit_in_bytes\n\t\t\tcgroup_controller.write_value('freezer', uuid, 'freezer.state', 'FROZEN')\n\t\t\tcgroup_controller.write_value('memory', uuid, 'memory.limit_in_bytes', mem_phy_quota)\n\t\t\tcgroup_controller.write_value('memory', uuid, 'memory.limit_in_bytes', mem_phy_quota)\n\t\t\tcgroup_controller.write_value('memory', uuid, 'memory.memsw.limit_in_bytes', mem_page_quota)\n\t\t\tcgroup_controller.write_value('freezer', uuid, 'freezer.state', 'THAWED')\n\t\n\tdef set_container_physical_memory_limit(uuid, Mbytes, freeze = False):\n\t\tif freeze:\n\t\t\tcgroup_controller.write_value('freezer', uuid, 'freezer.state', 'FROZEN')\n\t\tmemory_limit = int(max(0, Mbytes)) << 20\n\t\tcgroup_controller.write_value('memory', uuid, 'memory.limit_in_bytes', memory_limit)\n\t\tif freeze:\n\t\t\tcgroup_controller.write_value('freezer', uuid, 'freezer.state', 'THAWED')\n\t\t\n\tdef set_container_cpu_priority_limit(uuid, ceof):\n\t\tcpu_scaling = min(1024, 10 + int(1024 * ceof))\n\t\tcgroup_controller.write_value('cpu', uuid, 'cpu.shares', cpu_scaling)\n"
  },
  {
    "path": "meter/intra/smart.py",
    "content": "import subprocess, time, os, threading, math\n\nfrom intra.system import system_manager\nfrom intra.cgroup import cgroup_manager\nfrom intra.billing import billing_manager\n\nclass smart_controller:\n\t\n\tdef set_policy(policy):\n\t\tsmart_controller.policy = policy\n\t\n\tdef start(interval = 4):\n\t\tthread = threading.Thread(target = smart_controller.smart_control_forever, args = [interval])\n\t\tthread.setDaemon(True)\n\t\tthread.start()\n\t\treturn thread\n\t\n\tdef smart_control_forever(interval):\n\t\tlast_live = []\n\t\twhile True:\n\t\t\ttime.sleep(interval)\n\t\t\ttry:\n\t\t\t\tmem_usage_mapping = {}\n\t\t\t\tlive = cgroup_manager.get_cgroup_containers()\n\t\t\t\tfor item in live:\n\t\t\t\t\ttry:\n\t\t\t\t\t\tlast_live.remove(item)\n\t\t\t\t\texcept:\n\t\t\t\t\t\tpass\n\t\t\t\t\ttry:\n\t\t\t\t\t\tcgroup_manager.protect_container_oom(item)\n\t\t\t\t\t\tsample = cgroup_manager.get_container_sample(item)\n\t\t\t\t\t\tmem_usage_mapping[item] = math.ceil(sample['mem_page_sample'] * 1e-6)\n\t\t\t\t\t\tbilling_manager.add_usage_sample(item, sample, interval)\n\t\t\t\t\texcept:\n\t\t\t\t\t\tpass\n\t\t\t\tfor item in last_live:\n\t\t\t\t\tbilling_manager.clean_dead_node(item)\n\t\t\t\tlast_live = live\n\t\t\t\tis_ready = True\n\t\t\t\t\n\t\t\t\tmemory_available = system_manager.get_available_memsw()\n\t\t\t\tif memory_available['Mbytes'] <= 0:\n\t\t\t\t\tsize_in_gb = int(math.ceil(-memory_available['Mbytes'] / 1024 / 16) * 16)\n\t\t\t\t\tprint(\"[warning]\", 'overloaded containers, auto-extending %d G memsw.' % size_in_gb)\n\t\t\t\t\tsystem_manager.extend_swap(size_in_gb)\n\t\t\t\t\n\t\t\t\ttotal_score = 0.0\n\t\t\t\tscore_mapping = {}\n\t\t\t\tfor item in live:\n\t\t\t\t\tscore = max(1e-8, smart_controller.policy.get_score_by_uuid(item))\n\t\t\t\t\tscore_mapping[item] = score\n\t\t\t\t\tprint(item, \"(score/cpu)\", score)\n\t\t\t\t\ttotal_score += score\n\t\t\t\t\n\t\t\t\t# CPU Scoring\n\t\t\t\tfor item in live:\n\t\t\t\t\tceof = score_mapping[item] / total_score\n\t\t\t\t\tcgroup_manager.set_container_cpu_priority_limit(item, ceof)\n\t\t\t\t\n\t\t\t\t# Iterative Memory Scoring\n\t\t\t\tfree_mem = system_manager.get_total_physical_memory_for_containers()['Mbytes']\n\t\t\t\tlocal_nodes = live\n\t\t\t\tmem_alloc = {}\n\t\t\t\tfor item in live:\n\t\t\t\t\tmem_alloc[item] = 0\n\t\t\t\t\n\t\t\t\twhile free_mem > 0 and len(local_nodes) > 0:\n\t\t\t\t\texcess_mem = 0\n\t\t\t\t\tnext_local_nodes = []\n\t\t\t\t\tfor item in local_nodes:\n\t\t\t\t\t\tmem_alloc[item] += int(math.floor(free_mem * score_mapping[item] / total_score))\n\t\t\t\t\t\tif mem_alloc[item] >= mem_usage_mapping[item]:\n\t\t\t\t\t\t\texcess_mem += mem_alloc[item] - mem_usage_mapping[item]\n\t\t\t\t\t\t\tmem_alloc[item] = mem_usage_mapping[item]\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\tnext_local_nodes.append(item)\n\t\t\t\t\tfree_mem = excess_mem\n\t\t\t\t\tlocal_nodes = next_local_nodes\n\t\t\t\t\n\t\t\t\tfor item in live:\n\t\t\t\t\tmem_alloc[item] += int(math.floor(free_mem * score_mapping[item] / total_score))\n\t\t\t\t\tcgroup_manager.set_container_physical_memory_limit(item, mem_alloc[item])\n\t\t\t\t\tprint(item, \"(malloc:usage)\", mem_alloc[item], mem_usage_mapping[item])\n\t\t\t\t\n\t\t\t\tif len(live) > 0:\n\t\t\t\t\tprint(\"-------------------------------\")\n\t\t\t\t\n\t\t\texcept:\n\t\t\t\tpass\n\n\n# echo \"8:0 1000\" > /sys/fs/cgroup/blkio/lxc/docklet-1-0/blkio.throttle.write_bps_device\n# https://www.kernel.org/doc/Documentation/devices.txt\n# while true; do clear; cat /sys/fs/cgroup/blkio/lxc/docklet-1-0/blkio.throttle.io_service_bytes; sleep 0.5; done\n# hugetlb, net_cls, net_prio, /sbin/tc\n"
  },
  {
    "path": "meter/intra/system.py",
    "content": "import subprocess, time, os\n\nfrom intra.cgroup import cgroup_manager\n\nclass system_manager:\n\t\n\tdb_prefix = '.'\n\t\n\tdef set_db_prefix(prefix):\n\t\tsystem_manager.db_prefix = prefix\n\t\ttry:\n\t\t\tos.makedirs(prefix)\n\t\texcept:\n\t\t\tpass\n\t\n\tdef clear_all_swaps():\n\t\tsubprocess.getoutput('swapoff -a')\n\t\tsubprocess.getoutput('losetup -D')\n\t\n\tdef extend_swap(size):\n\t\tif size < 0:\n\t\t\t(mem_free, mem_total) = system_manager.get_memory_sample()\n\t\t\tsize = (mem_total + mem_total // 8) // 1024\n\t\tnid = 128\n\t\twhile subprocess.getoutput(\"cat /proc/swaps | grep cg-loop | awk '{print $1}' | awk -F\\- '{print $NF}' | grep %d$\" % nid) != \"\":\n\t\t\tnid = nid + 1\n\t\tstart_time = time.time()\n\t\t# setup\n\t\tos.system('dd if=/dev/zero of=/tmp/cg-swap-%d bs=1G count=0 seek=%d >/dev/null 2>&1' % (nid, size))\n\t\tos.system('mknod -m 0660 /dev/cg-loop-%d b 7 %d >/dev/null 2>&1' % (nid, nid))\n\t\tos.system('losetup /dev/cg-loop-%d /tmp/cg-swap-%d >/dev/null 2>&1' % (nid, nid))\n\t\tos.system('mkswap /dev/cg-loop-%d >/dev/null 2>&1' % nid)\n\t\tsuccess = os.system('swapon /dev/cg-loop-%d >/dev/null 2>&1' % nid) == 0\n\t\t# detach\n\t\t# os.system('swapoff /dev/cg-loop-%d >/dev/null 2>&1' % nid)\n\t\t# os.system('losetup -d /dev/cg-loop-%d >/dev/null 2>&1' % nid)\n\t\t# os.system('rm -f /dev/cg-loop-%d /tmp/cg-swap-%d >/dev/null 2>&1' % (nid, nid))\n\t\tend_time = time.time()\n\t\treturn {\"setup\": success, \"time\": end_time - start_time }\n\n\tdef get_cpu_sample():\n\t\t[a, b, c, d] = subprocess.getoutput(\"cat /proc/stat | grep ^cpu\\  | awk '{print $2, $3, $4, $6}'\").split()\n\t\tcpu_time = int(a) + int(b) + int(c) + int(d)\n\t\treturn (cpu_time, time.time())\n\n\tdef get_memory_sample():\n\t\tmem_free = int(subprocess.getoutput(\"awk '{if ($1==\\\"MemAvailable:\\\") print $2}' /proc/meminfo 2>/dev/null\")) // 1024\n\t\tmem_total = int(subprocess.getoutput(\"awk '{if ($1==\\\"MemTotal:\\\") print $2}' /proc/meminfo 2>/dev/null\")) // 1024\n\t\treturn (mem_free, mem_total)\n\n\tdef get_swap_sample():\n\t\tswap_free = int(subprocess.getoutput(\"awk '{if ($1==\\\"SwapFree:\\\") print $2}' /proc/meminfo 2>/dev/null\")) // 1024\n\t\tswap_total = int(subprocess.getoutput(\"awk '{if ($1==\\\"SwapTotal:\\\") print $2}' /proc/meminfo 2>/dev/null\")) // 1024\n\t\treturn (swap_free, swap_total)\n\n\tdef get_system_loads():\n\t\tif 'last_cpu_sample' not in system_manager.__dict__:\n\t\t\tsystem_manager.last_cpu_sample = system_manager.get_cpu_sample()\n\t\t\ttime.sleep(1)\n\t\tcpu_sample = system_manager.get_cpu_sample()\n\t\t(mem_free, mem_total) = system_manager.get_memory_sample()\n\t\t(swap_free, swap_total) = system_manager.get_swap_sample()\n\t\tncpus = int(subprocess.getoutput(\"grep processor /proc/cpuinfo | wc -l\"))\n\t\tcpu_free = ncpus - (cpu_sample[0] - system_manager.last_cpu_sample[0]) * 0.01 / (cpu_sample[1] - system_manager.last_cpu_sample[1])\n\t\tcpu_free = 0.0 if cpu_free <= 0.0 else cpu_free\n\t\tsystem_manager.last_cpu_sample = cpu_sample\n\t\treturn {\"mem_free\": mem_free, \"mem_total\": mem_total, \"swap_free\": swap_free, \"swap_total\": swap_total, \"cpu_free\": cpu_free, \"cpu_total\": ncpus }\n\t\n\tdef get_proc_etime(pid):\n\t\tfmt = subprocess.getoutput(\"ps -A -opid,etime | grep '^ *%d' | awk '{print $NF}'\" % pid).strip()\n\t\tif fmt == '':\n\t\t\treturn -1\n\t\tparts = fmt.split('-')\n\t\tdays = int(parts[0]) if len(parts) == 2 else 0\n\t\tfmt = parts[-1]\n\t\tparts = fmt.split(':')\n\t\thours = int(parts[0]) if len(parts) == 3 else 0\n\t\tparts = parts[len(parts)-2:]\n\t\tminutes = int(parts[0])\n\t\tseconds = int(parts[1])\n\t\treturn ((days * 24 + hours) * 60 + minutes) * 60 + seconds\n\n\tdef get_available_memsw():\n\t\ttotal_mem_limit = 0\n\t\ttotal_mem_used = 0\n\t\tsysloads = system_manager.get_system_loads()\n\t\tlive = cgroup_manager.get_cgroup_containers()\n\t\t\n\t\tfor item in live:\n\t\t\ttry:\n\t\t\t\tsample = cgroup_manager.get_container_sample(item)\n\t\t\t\tlimit = cgroup_manager.get_container_limit(item)\n\t\t\t\ttotal_mem_limit += limit[\"mem_page_quota\"]\n\t\t\t\ttotal_mem_used += sample[\"mem_page_sample\"]\n\t\t\texcept:\n\t\t\t\tpass\n\t\t\n\t\ttotal_mem_limit >>= 20\n\t\ttotal_mem_used = (total_mem_used + (1<<20) - 1) >> 20\n\t\t\n\t\tavailable_mem_resource = sysloads['mem_free'] + \\\n\t\t\tsysloads['swap_free'] - total_mem_limit + total_mem_used\n\t\treturn {\"Mbytes\": available_mem_resource, \"physical\": sysloads['mem_free'], \"cpu_free\": sysloads['cpu_free']}\n\n\tdef get_total_physical_memory_for_containers():\n\t\ttotal_mem_used = 0\n\t\tsysloads = system_manager.get_system_loads()\n\t\tlive = cgroup_manager.get_cgroup_containers()\n\t\t\n\t\tfor item in live:\n\t\t\ttry:\n\t\t\t\tsample = cgroup_manager.get_container_sample(item)\n\t\t\t\ttotal_mem_used += sample[\"mem_page_sample\"]\n\t\t\texcept:\n\t\t\t\tpass\n\t\t\n\t\ttotal_mem_used = (total_mem_used + (1<<20) - 1) >> 20\n\t\ttotal_physical_memory_for_containers = sysloads['mem_free'] + total_mem_used\n\t\t\n\t\treturn {\"Mbytes\": total_physical_memory_for_containers}\n\n"
  },
  {
    "path": "meter/main.py",
    "content": "#!/usr/bin/python3\n\n########################################\n# Boot for Local:\n#   sudo ./main (or: sudo ./main [master-ipaddr])\n#\n\n########################################\n# Usage for Local:\n#    curl -F uuid=\"lxc-name1\" http://0.0.0.0:1729/v1/cgroup/container/sample\n#\n\nimport time, sys, signal, json, subprocess, os\n\nif __name__ == '__main__':\n\tif not subprocess.getoutput('lsb_release -r -s 2>/dev/null').startswith('16.04'):\n\t\traise Exception('Ubuntu 16.04 LTS is required.')\n\t\n\tif not os.path.exists('/sys/fs/cgroup/memory/memory.memsw.usage_in_bytes'):\n\t\traise Exception('Please append \"swapaccount=1\" to kernel.')\n\t\n\tif subprocess.getoutput('whoami') != 'root':\n\t\traise Exception('Root privilege is required.')\n\t\n\tfrom daemon.http import *\n\tif len(sys.argv) == 1:\n\t\tsys.argv.append('disable-network')\n\t\n\tdef signal_handler(signal, frame):\n\t\tif sys.argv[1] == 'master':\n\t\t\tsubprocess.getoutput('ovs-vsctl del-br ovs-master >/dev/null 2>&1')\n\t\telse:\n\t\t\tsubprocess.getoutput('ovs-vsctl del-br ovs-minion >/dev/null 2>&1')\n\t\tsys.exit(0)\n\tsignal.signal(signal.SIGINT, signal_handler)\n\t\n\tif sys.argv[1] != 'master': # for minions\n\t\tfrom intra.cgroup import cgroup_manager\n\t\tcgroup_manager.auto_detect_prefix()\n\t\tcgroup_manager.set_default_memory_limit(4)\n\n\t\tfrom intra.system import system_manager\n\t\tsystem_manager.set_db_prefix('/var/lib/docklet/meter')\n\t\t# system_manager.extend_swap(32)\n\t\t\n\t\tif sys.argv[1] != 'disable-network':\n\t\t\tfrom connector.minion import minion_connector\n\t\t\tminion_connector.start(sys.argv[1])\n\t\telse:\n\t\t\tprint(\"(No network mode)\")\n\n\t\tfrom policy.quota import identify_policy\n\t\tfrom intra.smart import smart_controller\n\t\t\n\t\tsmart_controller.set_policy(identify_policy)\n\t\tsmart_controller.start()\n\t\t\n\t\tprint(\"Minion REST Daemon Starts Listening ..\")\n\t\thttp = http_daemon_listener(minion_http_handler)\n\t\thttp.listen()\n\t\t\n\telse: # for master: sudo ./main master\n\t\tfrom connector.master import master_connector\n\t\tmaster_connector.start()\n\t\t\n\t\tprint(\"Master REST Daemon Starts Listening ..\")\n\t\thttp = http_daemon_listener(master_http_handler, master_connector)\n\t\thttp.listen()\n\n"
  },
  {
    "path": "meter/policy/allocate.py",
    "content": "class candidates_selector:\n\t\n\tdef select(candidates):\n\t\treturn max(candidates, key=lambda addr: candidates[addr]['cpu_free'])\n\n"
  },
  {
    "path": "meter/policy/quota.py",
    "content": "from intra.system import system_manager\nfrom intra.cgroup import cgroup_manager\nimport subprocess\n\nclass identify_policy:\n\t\n\tdef get_score_by_uuid(uuid):\n\t\treturn 1.0\n\nclass etime_rev_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tpid = cgroup_manager.get_container_pid(uuid)\n\t\tetime = system_manager.get_proc_etime(pid)\n\t\treturn 1.0 / (1.0 + etime)\n\nclass mem_usage_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tsample = cgroup_manager.get_container_sample(uuid)\n\t\treturn sample[\"mem_page_sample\"]\n\nclass mem_quota_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tsample = cgroup_manager.get_container_limit(uuid)\n\t\treturn sample[\"mem_page_quota\"]\n\nclass cpu_usage_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tsample = cgroup_manager.get_container_sample(uuid)\n\t\treturn sample[\"cpu_sample\"]\n\nclass cpu_usage_rev_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tsample = cgroup_manager.get_container_sample(uuid)\n\t\treturn 1024 * 1024 / (1.0 + sample[\"cpu_sample\"])\n\nclass cpu_speed_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tsample = cgroup_manager.get_container_sample(uuid)\n\t\tpid = cgroup_manager.get_container_pid(uuid)\n\t\tetime = system_manager.get_proc_etime(pid)\n\t\treturn sample[\"cpu_sample\"] / etime\n\nclass user_state_policy(identify_policy):\n\t\n\tdef get_score_by_uuid(uuid):\n\t\tuser = uuid.split('-')[0]\n\t\tonline = subprocess.getoutput('cat /var/lib/docklet/global/users/%s/status 2>/dev/null' % user) == 'live'\n\t\treturn 10.0 if online else 1.0\n\n\n"
  },
  {
    "path": "prepare.sh",
    "content": "#!/bin/bash\n\n##################################################\n#                before-start.sh\n# when you first use docklet, you should run this script to\n# check and prepare the environment\n# *important* : you need run this script again and again till success\n##################################################\n\nif [[ \"`whoami`\" != \"root\" ]]; then\n\techo \"FAILED: Require root previledge !\" > /dev/stderr\n\texit 1\nfi\n\n# install packages that docklet needs (in ubuntu)\n# some packages' name maybe different in debian\napt-get install -y lxc lxcfs lxc-templates lvm2 bridge-utils curl exim4 openssh-server openvswitch-switch\napt-get install -y python3 python3-netifaces python3-flask python3-flask-sqlalchemy python3-pampy python3-httplib2 python3-pip\napt-get install -y python3-psutil python3-flask-migrate python3-paramiko\napt-get install -y python3-lxc\napt-get install -y python3-requests python3-suds\napt-get install -y nodejs npm\napt-get install -y etcd\napt-get install -y glusterfs-client attr\napt-get install -y nginx\npip3 install Flask-WTF\napt-get install -y gdebi-core\npip3 install grpcio grpcio-tools googleapis-common-protos\n\n#add ip forward\necho \"net.ipv4.ip_forward=1\" >>/etc/sysctl.conf\nsysctl -p\n\n# check cgroup control\n#which cgm &> /dev/null || { echo \"FAILED : cgmanager is required, please install cgmanager\" && exit 1; }\n#cpucontrol=$(cgm listkeys cpu)\n#[[ -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\n#memcontrol=$(cgm listkeys memory)\n#if [[ -z $(echo $memcontrol | grep limit_in_bytes) ]]; then\n#\techo \"FAILED : memory.limit_in_bytes of cgroup is not supported\"\n#\techo \"Try : \"\n#\techo -e \"  echo 'GRUB_CMDLINE_LINUX=\\\"cgroup_enable=memory swapaccount=1\\\"' >> /etc/default/grub; update-grub; reboot\" > /dev/stderr\n#\techo \"Info : if not success, you may need to recompile kernel\"\n#\texit 1\n#fi\n\n\n# check and install configurable-http-proxy\nwhich configurable-http-proxy &>/dev/null || { npm config set registry https://registry.npm.taobao.org && npm install -g configurable-http-proxy; }\nwhich configurable-http-proxy &>/dev/null || { echo \"Error: install configurable-http-proxy failed, you should try again\" && exit 1; }\n\necho \"\"\n[[ -f conf/docklet.conf ]] || { echo \"Generating docklet.conf from template\" && cp conf/docklet.conf.template conf/docklet.conf; }\n[[ -f web/templates/home.html ]] || { echo \"Generating HomePage from home.template\" && cp web/templates/home.template web/templates/home.html; }\n\nFS_PREFIX=/opt/docklet\n. conf/docklet.conf\nexport FS_PREFIX\n\nmkdir -p $FS_PREFIX/global\nmkdir -p $FS_PREFIX/local/\n\necho \"directory FS_PREFIX (${FS_PREFIX}) have been created\"\n\nif [[ ! -d $FS_PREFIX/local/basefs && ! $1 = \"withoutfs\" ]]; then\n\tmkdir -p $FS_PREFIX/local/basefs\n\techo \"Generating basefs\"\n\t# wget -P $FS_PREFIX/local http://iwork.pku.edu.cn:1616/basefs-0.11.tar.bz2 && tar xvf $FS_PREFIX/local/basefs-0.11.tar.bz2 -C $FS_PREFIX/local/ > /dev/null\n\t[ $? != \"0\" ] && echo \"Generate basefs failed, please download it from http://unias.github.io/docklet/download to FS_PREFIX/local and then extract it using root. (defalut FS_PRERIX is /opt/docklet)\"\nfi\n\necho \"Some packagefs can be downloaded from http://unias.github.io/docklet.download\"\necho \"you can download the packagefs and extract it to FS_PREFIX/local using root. (default FS_PREFIX is /opt/docklet\"\n\necho \"\"\necho \"All preparation installations are done.\"\necho \"****************************************\"\necho \"* Please Read Lines Below Before Start *\"\necho \"****************************************\"\necho \"\"\n\necho \"you may want to custom home page of docklet. Please modify web/templates/home.html\"\n\necho \"Next, make sure exim4 can deliver mail out. To enable, run:\"\necho \"dpkg-reconfigure exim4-config\"\necho \"select internet site\"\n\necho \"\"\necho \"Then start docklet as described in README.md\"\n"
  },
  {
    "path": "src/master/beansapplicationmgr.py",
    "content": "#!/usr/bin/python3\n\n'''\nThis module consists of three parts:\n1.send_beans_email: a function to send email to remind users of their beans.\n\n2.ApplicationMgr: a class that will deal with users' requests about beans application.\n\n3.ApprovalRobot: a automatic robot to examine and approve users' applications.\n\n'''\n\nimport threading,datetime,random,time\nfrom utils.model import db,User,ApplyMsg\nfrom master.userManager import administration_required\nfrom utils import env\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header\nfrom master.settings import settings\n\n\n# send email to remind users of their beans\ndef send_beans_email(to_address, username, beans):\n    email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n    if (email_from_address in ['\\'\\'', '\\\"\\\"', '']):\n        return\n    #text = 'Dear '+ username + ':\\n' + '  Your beans in docklet are less than' + beans + '.'\n    text = '<html><h4>Dear '+ username + ':</h4>'\n    text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Your beans in <a href='%s'>docklet</a> are %d now. </p>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;If your beans are less than or equal to 0, all your worksapces will be stopped.</p>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Please apply for more beans to keep your workspaces running by following link:</p>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href='%s/beans/application/'>%s/beans/application/</p>\n               <br>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Note: DO NOT reply to this email!</p>\n               <br><br>\n               <p> <a href='http://docklet.unias.org'>Docklet Team</a>, SEI, PKU</p>\n            ''' % (env.getenv(\"PORTAL_URL\"), beans, env.getenv(\"PORTAL_URL\"), env.getenv(\"PORTAL_URL\"))\n    text += '<p>'+  str(datetime.datetime.now()) + '</p>'\n    text += '</html>'\n    subject = 'Docklet beans alert'\n    msg = MIMEMultipart()\n    textmsg = MIMEText(text,'html','utf-8')\n    msg['Subject'] = Header(subject, 'utf-8')\n    msg['From'] = email_from_address\n    msg['To'] = to_address\n    msg.attach(textmsg)\n    s = smtplib.SMTP()\n    s.connect()\n    s.sendmail(email_from_address, to_address, msg.as_string())\n    s.close()\n\n# a class that will deal with users' requests about beans application.\nclass ApplicationMgr:\n\n    def __init__(self):\n        # create database\n        try:\n            ApplyMsg.query.all()\n        except:\n            db.create_all()\n\n    # user apply for beans\n    def apply(self,username,number,reason):\n        user = User.query.filter_by(username=username).first()\n        if user is not None and user.beans >= 1000:\n            return [False, \"Your beans must be less than 1000.\"]\n        if int(number) < 100 or int(number) > 5000:\n            return [False, \"Number field must be between 100 and 5000!\"]\n        applymsgs = ApplyMsg.query.filter_by(username=username).all()\n        lasti = len(applymsgs) - 1      # the last index, the last application is also the latest application.\n        if lasti >= 0 and applymsgs[lasti].status == \"Processing\":\n            return [False, \"You already have a processing application, please be patient.\"]\n        # store the application into the database\n        applymsg = ApplyMsg(username,number,reason)\n        db.session.add(applymsg)\n        db.session.commit()\n        return [True,\"\"]\n\n    # get all applications of a user\n    def query(self,username):\n        applymsgs = ApplyMsg.query.filter_by(username=username).all()\n        ans = []\n        for msg in applymsgs:\n            ans.append(msg.ch2dict())\n        return ans\n\n    # get all unread applications\n    @administration_required\n    def queryUnRead(self,*,cur_user):\n        applymsgs = ApplyMsg.query.filter_by(status=\"Processing\").all()\n        ans = []\n        for msg in applymsgs:\n            ans.append(msg.ch2dict())\n        return {\"success\":\"true\",\"applymsgs\":ans}\n\n    # agree an application\n    @administration_required\n    def agree(self,msgid,*,cur_user):\n        applymsg = ApplyMsg.query.get(msgid)\n        if applymsg is None:\n            return {\"success\":\"false\",\"message\":\"Application doesn\\'t exist.\"}\n        applymsg.status = \"Agreed\"\n        user = User.query.filter_by(username=applymsg.username).first()\n        if user is not None:\n            # update users' beans\n            user.beans += applymsg.number\n        db.session.commit()\n        return {\"success\":\"true\"}\n\n    # reject an application\n    @administration_required\n    def reject(self,msgid,*,cur_user):\n        applymsg = ApplyMsg.query.get(msgid)\n        if applymsg is None:\n            return {\"success\":\"false\",\"message\":\"Application doesn\\'t exist.\"}\n        applymsg.status = \"Rejected\"\n        db.session.commit()\n        return {\"success\":\"true\"}\n\n# a automatic robot to examine and approve users' applications.\nclass ApprovalRobot(threading.Thread):\n\n    def __init__(self,maxtime=3600):\n        threading.Thread.__init__(self)\n        self.stop = False\n        self.interval = 20\n        self.maxtime = maxtime      # The max time that users may wait for from 'processing' to 'agreed'\n\n    def stop(self):\n        self.stop = True\n\n    def run(self):\n        while not self.stop:\n            # query all processing applications\n            applymsgs = ApplyMsg.query.filter_by(status=\"Processing\").all()\n            for msg in applymsgs:\n                secs = (datetime.datetime.now() - msg.time).seconds\n                #ranint = random.randint(self.interval,self.maxtime)\n                if secs >= self.maxtime:\n                    msg.status = \"Agreed\"\n                    user = User.query.filter_by(username=msg.username).first()\n                    if user is not None:\n                    # update users'beans\n                        user.beans += msg.number\n                    db.session.commit()\n            time.sleep(self.interval)\n"
  },
  {
    "path": "src/master/bugreporter.py",
    "content": "from master.settings import settings\nimport smtplib\nfrom utils.log import logger\nfrom utils import env\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header\nfrom datetime import datetime\nimport json\n\ndef send_bug_mail(username, bugmessage):\n    #admin_email_address = env.getenv('ADMIN_EMAIL_ADDRESS')\n    nulladdr = ['\\'\\'', '\\\"\\\"', '']\n    email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n    admin_email_address = settings.get('ADMIN_EMAIL_ADDRESS')\n    logger.info(\"receive bug from %s: %s\" % (username, bugmessage))\n    if (email_from_address in nulladdr or admin_email_address in nulladdr):\n        return {'success': 'false'}\n    #text = 'Dear '+ username + ':\\n' + '  Your account in docklet has been activated'\n    text = '<html><h4>Dear '+ 'admin' + ':</h4>'\n    text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;A bug has been report by %s.</p>\n               <br/>\n               <strong>&nbsp; %s &nbsp;</strong>\n               <br/>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Please check it !</p>\n               <br/><br/>\n               <p> Docklet Team, SEI, PKU</p>\n            ''' % (username, bugmessage)\n    text += '<p>'+  str(datetime.utcnow()) + '</p>'\n    text += '</html>'\n    subject = 'A bug of Docklet has been reported'\n    if admin_email_address[0] == '\"':\n        admins_addr = admin_email_address[1:-1].split(\" \")\n    else:\n        admins_addr = admin_email_address.split(\" \")\n    alladdr=\"\"\n    for addr in admins_addr:\n        alladdr = alladdr+addr+\", \"\n    alladdr=alladdr[:-2]\n    msg = MIMEMultipart()\n    textmsg = MIMEText(text,'html','utf-8')\n    msg['Subject'] = Header(subject, 'utf-8')\n    msg['From'] = email_from_address\n    msg['To'] = alladdr\n    msg.attach(textmsg)\n    s = smtplib.SMTP()\n    s.connect()\n    try:\n        s.sendmail(email_from_address, admins_addr, msg.as_string())\n    except Exception as e:\n        logger.error(e)\n    s.close()\n    return {'success':'true'}\n"
  },
  {
    "path": "src/master/cloudmgr.py",
    "content": "#!/usr/bin/python3\nfrom io import StringIO\nimport os,sys,subprocess,time,re,datetime,threading,random,shutil\nfrom utils.model import db, Image\nfrom master.deploy import *\nimport json\n\nfrom utils.log import logger\nfrom utils import env\nimport requests\n\nfspath = env.getenv('FS_PREFIX')\n\n\nclass AliyunMgr():\n    def __init__(self):\n        self.AcsClient = __import__('aliyunsdkcore.client', fromlist=[\"AcsClient\"])\n        self.Request = __import__('aliyunsdkecs.request.v20140526', fromlist=[\n            \"CreateInstanceRequest\",\n            \"StopInstanceRequest\",\n            \"DescribeInstancesRequest\",\n            \"DeleteInstanceRequest\",\n            \"StartInstanceRequest\",\n            \"DescribeInstancesRequest\",\n            \"AllocateEipAddressRequest\",\n            \"AssociateEipAddressRequest\"])\n\n    def loadClient(self):\n        if not os.path.exists(fspath+\"/global/sys/cloudsetting.json\"):\n            currentfilepath = os.path.dirname(os.path.abspath(__file__))\n            templatefilepath = currentfilepath + \"/../tools/cloudsetting.aliyun.template.json\"\n            shutil.copyfile(templatefilepath,fspath+\"/global/sys/cloudsetting.json\")\n            logger.error(\"please modify the setting file first\")\n            return False\n        try:\n            settingfile = open(fspath+\"/global/sys/cloudsetting.json\", 'r')\n            self.setting = json.loads(settingfile.read())\n            settingfile.close()\n            self.clt = self.AcsClient.AcsClient(self.setting['AccessKeyId'],self.setting['AccessKeySecret'], self.setting['RegionId'])\n            logger.info(\"load CLT of Aliyun success\")\n            return True\n        except Exception as e:\n            logger.error(e)\n            return False\n\n    def createInstance(self):\n        request = self.Request.CreateInstanceRequest.CreateInstanceRequest()\n        request.set_accept_format('json')\n        request.add_query_param('RegionId', self.setting['RegionId'])\n        if 'ZoneId' in self.setting and not self.setting['ZoneId'] == \"\":\n            request.add_query_param('ZoneId', self.setting['ZoneId'])\n        if 'VSwitchId' in self.setting and not self.setting['VSwitchId'] == \"\":\n            request.add_query_param('VSwitchId', self.setting['VSwitchId'])\n        request.add_query_param('ImageId', 'ubuntu_16_0402_64_20G_alibase_20170818.vhd')\n        request.add_query_param('InternetMaxBandwidthOut', 1)\n        request.add_query_param('InstanceName', 'docklet_tmp_worker')\n        request.add_query_param('HostName', 'worker-tmp')\n        request.add_query_param('SystemDisk.Size', int(self.setting['SystemDisk.Size']))\n        request.add_query_param('InstanceType', self.setting['InstanceType'])\n        request.add_query_param('Password', self.setting['Password'])\n        response = self.clt.do_action_with_exception(request)\n        logger.info(response)\n\n        instanceid=json.loads(bytes.decode(response))['InstanceId']\n        return instanceid\n\n    def startInstance(self, instanceid):\n        request = self.Request.StartInstanceRequest.StartInstanceRequest()\n        request.set_accept_format('json')\n        request.add_query_param('InstanceId', instanceid)\n        response = self.clt.do_action_with_exception(request)\n        logger.info(response)\n\n\n    def createEIP(self):\n        request = self.Request.AllocateEipAddressRequest.AllocateEipAddressRequest()\n        request.set_accept_format('json')\n        request.add_query_param('RegionId', self.setting['RegionId'])\n        response = self.clt.do_action_with_exception(request)\n        logger.info(response)\n\n        response=json.loads(bytes.decode(response))\n        eipid=response['AllocationId']\n        eipaddr=response['EipAddress']\n\n        return [eipid, eipaddr]\n\n\n    def associateEIP(self, instanceid, eipid):\n        request = self.Request.AssociateEipAddressRequest.AssociateEipAddressRequest()\n        request.set_accept_format('json')\n        request.add_query_param('AllocationId', eipid)\n        request.add_query_param('InstanceId', instanceid)\n        response = self.clt.do_action_with_exception(request)\n        logger.info(response)\n\n\n    def getInnerIP(self, instanceid):\n        request = self.Request.DescribeInstancesRequest.DescribeInstancesRequest()\n        request.set_accept_format('json')\n        response = self.clt.do_action_with_exception(request)\n        instances = json.loads(bytes.decode(response))['Instances']['Instance']\n        for instance in instances:\n            if instance['InstanceId'] == instanceid:\n                return instance['NetworkInterfaces']['NetworkInterface'][0]['PrimaryIpAddress']\n        return json.loads(bytes.decode(response))['Instances']['Instance'][0]['VpcAttributes']['PrivateIpAddress']['IpAddress'][0]\n\n    def isStarted(self, instanceids):\n        request = self.Request.DescribeInstancesRequest.DescribeInstancesRequest()\n        request.set_accept_format('json')\n        response = self.clt.do_action_with_exception(request)\n        instances = json.loads(bytes.decode(response))['Instances']['Instance']\n        for instance in instances:\n            if instance['InstanceId'] in instanceids:\n                if not instance['Status'] == \"Running\":\n                    return False\n        return True\n\n    def rentServers(self,number):\n        instanceids=[]\n        eipids=[]\n        eipaddrs=[]\n        for i in range(int(number)):\n            instanceids.append(self.createInstance())\n            time.sleep(2)\n        time.sleep(10)\n        for i in range(int(number)):\n            [eipid,eipaddr]=self.createEIP()\n            eipids.append(eipid)\n            eipaddrs.append(eipaddr)\n            time.sleep(2)\n        masterip=env.getenv('ETCD').split(':')[0]\n        for i in range(int(number)):\n            self.associateEIP(instanceids[i],eipids[i])\n            time.sleep(2)\n        time.sleep(5)\n        for instanceid in instanceids:\n            self.startInstance(instanceid)\n            time.sleep(2)\n        time.sleep(10)\n        while not self.isStarted(instanceids):\n            time.sleep(10)\n        time.sleep(5)\n        return [masterip, eipaddrs]\n\n    def addNode(self):\n        if not self.loadClient():\n            return {'success':'false'}\n        [masterip, eipaddrs] = self.rentServers(1)\n        threads = []\n        for eip in eipaddrs:\n            thread = threading.Thread(target = deploy, args=(eip,masterip,'root',self.setting['Password'],self.setting['VolumeName']))\n            thread.setDaemon(True)\n            thread.start()\n            threads.append(thread)\n        for thread in threads:\n            thread.join()\n        return {'success':'true'}\n\n    def addNodeAsync(self):\n        thread = threading.Thread(target = self.addNode)\n        thread.setDaemon(True)\n        thread.start()\n\nclass EmptyMgr():\n    def addNodeAsync(self):\n        logger.error(\"current cluster does not support scale out\")\n        return False\n\nclass CloudMgr():\n\n    def getSettingFile(self):\n        if not os.path.exists(fspath+\"/global/sys/cloudsetting.json\"):\n            currentfilepath = os.path.dirname(os.path.abspath(__file__))\n            templatefilepath = currentfilepath + \"/../tools/cloudsetting.aliyun.template.json\"\n            shutil.copyfile(templatefilepath,fspath+\"/global/sys/cloudsetting.json\")\n        settingfile = open(fspath+\"/global/sys/cloudsetting.json\", 'r')\n        setting = settingfile.read()\n        settingfile.close()\n        return {'success':'true', 'result':setting}\n\n    def modifySettingFile(self, setting):\n        if setting == None:\n            logger.error(\"setting is None\")\n            return {'success':'false'}\n        settingfile = open(fspath+\"/global/sys/cloudsetting.json\", 'w')\n        settingfile.write(setting)\n        settingfile.close()\n        return {'success':'true'}\n\n\n    def __init__(self):\n        if env.getenv(\"ALLOW_SCALE_OUT\") == \"True\":\n            self.engine = AliyunMgr()\n        else:\n            self.engine = EmptyMgr()\n"
  },
  {
    "path": "src/master/deploy.py",
    "content": "#!/usr/bin/python3\n\nimport paramiko, time, os\nfrom utils.log import logger\nfrom utils import env\n\ndef myexec(ssh,command):\n    stdin,stdout,stderr = ssh.exec_command(command)\n    endtime = time.time() + 3600\n    while not stdout.channel.eof_received:\n        time.sleep(2)\n        if time.time() > endtime:\n            stdout.channel.close()\n            logger.error(command + \": fail\")\n            return\n#    for line in stdout.readlines():\n#        if line is None:\n#            time.sleep(5)\n#        else:\n#            print(line)\n\ndef deploy(ipaddr,masterip,account,password,volumename):\n    while True:\n        try:\n            transport = paramiko.Transport((ipaddr,22))\n            transport.connect(username=account,password=password)\n            break\n        except Exception as e:\n            time.sleep(2)\n            pass\n    sftp = paramiko.SFTPClient.from_transport(transport)\n\n    currentfilepath = os.path.dirname(os.path.abspath(__file__))\n    deployscriptpath = currentfilepath + \"/../tools/docklet-deploy.sh\"\n    sftp.put(deployscriptpath,'/root/docklet-deploy.sh')\n    sftp.put('/etc/hosts', '/etc/hosts')\n    transport.close()\n\n    ssh = paramiko.SSHClient()\n    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())\n    while True:\n        try:\n            ssh.connect(ipaddr, username = account, password = password, timeout = 300)\n            break\n        except Exception as e:\n            time.sleep(2)\n            pass\n    myexec(ssh,\"sed -i 's/%MASTERIP%/\" + masterip + \"/g' /root/docklet-deploy.sh\")\n    myexec(ssh,\"sed -i 's/%VOLUMENAME%/\" + volumename + \"/g' /root/docklet-deploy.sh\")\n    myexec(ssh,'chmod +x /root/docklet-deploy.sh')\n    myexec(ssh,'/root/docklet-deploy.sh')\n    ssh.close()\n    return\n"
  },
  {
    "path": "src/master/httprest.py",
    "content": "#!/usr/bin/python3\n\n# load environment variables in the beginning\n# because some modules need variables when import\n# for example, userManager/model.py\n\nimport sys\nif sys.path[0].endswith(\"master\"):\n    sys.path[0] = sys.path[0][:-6]\nfrom flask import Flask, request\n\n# must first init loadenv\nfrom utils import tools, env\n# default CONFIG=/opt/docklet/local/docklet-running.conf\n\nconfig = env.getenv(\"CONFIG\")\ntools.loadenv(config)\n\n# second init logging\n# must import logger after initlogging, ugly\nfrom utils.log import initlogging\ninitlogging(\"docklet-master\")\nfrom utils.log import logger\n\nimport os\nimport http.server, cgi, json, sys, shutil, traceback\nimport xmlrpc.client\nfrom socketserver import ThreadingMixIn\nfrom utils import etcdlib, imagemgr\nfrom master import nodemgr, vclustermgr, notificationmgr, lockmgr, cloudmgr, jobmgr, taskmgr\nfrom utils.logs import logs\nfrom master import userManager, beansapplicationmgr, monitor, sysmgr, network, releasemgr\nfrom worker.monitor import History_Manager\nimport threading\nimport requests\nfrom utils.nettools import portcontrol\n\n#default EXTERNAL_LOGIN=False\nexternal_login = env.getenv('EXTERNAL_LOGIN')\nif (external_login == 'TRUE'):\n    from userDependence import external_auth\n\nuserpoint = \"http://\" + env.getenv('USER_IP') + \":\" + str(env.getenv('USER_PORT'))\nG_userip = env.getenv(\"USER_IP\")\n\ndef post_to_user(url = '/', data={}):\n    return requests.post(userpoint+url,data=data).json()\n\napp = Flask(__name__)\n\nfrom functools import wraps\n\n\ndef login_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        logger.info (\"get request, path: %s\" % request.path)\n        token = request.form.get(\"token\", None)\n        if (token == None):\n            logger.info (\"get request without token, path: %s\" % request.path)\n            return json.dumps({'success':'false', 'message':'user or key is null'})\n        result = post_to_user(\"/authtoken/\", {'token':token})\n        if result.get('success') == 'true':\n            username = result.get('username')\n            beans = result.get('beans')\n        else:\n            return result\n        #if (cur_user == None):\n        #    return json.dumps({'success':'false', 'message':'token failed or expired', 'Unauthorized': 'True'})\n        return func(username, beans, request.form, *args, **kwargs)\n\n    return wrapper\n\ndef auth_key_required(func):\n    @wraps(func)\n    def wrapper(*args,**kwargs):\n        key_1 = env.getenv('AUTH_KEY')\n        key_2 = request.form.get(\"auth_key\",None)\n        #logger.info(str(ip) + \" \" + str(G_userip))\n        if key_2 is not None and key_1 == key_2:\n           return func(*args, **kwargs)\n        else:\n           return json.dumps({'success':'false','message': 'auth_key is required!'})\n\n    return wrapper\n\ndef beans_check(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        beans = args[1]\n        if beans <= 0:\n            return json.dumps({'success':'false','message':'user\\'s beans are less than or equal to zero!'})\n        else:\n            return func(*args, **kwargs)\n\n    return wrapper\n\n@app.route(\"/isalive/\", methods = ['POST'])\n@login_required\ndef isalive(user, beans, form):\n    return json.dumps({'success':'true'})\n\n\n\n@app.route(\"/logs/list/\", methods=['POST'])\n@login_required\ndef logs_list(user, beans, form):\n    user_group = post_to_user('/user/selfQuery/', {'token': request.form.get(\"token\", None)}).get('data', None).get('group', None)\n    return json.dumps(logs.list(user_group = user_group))\n\n@app.route(\"/logs/get/\", methods=['POST'])\n@login_required\ndef logs_get(user, beans, form):\n    user_group = post_to_user('/user/selfQuery/', {'token': request.form.get(\"token\", None)}).get('data', None).get('group', None)\n    return json.dumps(logs.get(user_group = user_group, filename = form.get('filename', '')))\n\n\n@app.route(\"/cluster/create/\", methods=['POST'])\n@login_required\n@beans_check\ndef create_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        image = {}\n        image['name'] = form.get(\"imagename\", None)\n        image['type'] = form.get(\"imagetype\", None)\n        image['owner'] = form.get(\"imageowner\", None)\n        user_info = post_to_user(\"/user/selfQuery/\", {'token':form.get(\"token\")})\n        user_info = json.dumps(user_info)\n        logger.info (\"handle request : create cluster %s with image %s \" % (clustername, image['name']))\n        setting = {\n                'cpu': form.get('cpuSetting'),\n                'memory': form.get('memorySetting'),\n                'disk': form.get('diskSetting')\n                }\n        res = post_to_user(\"/user/usageInc/\", {'token':form.get('token'), 'setting':json.dumps(setting)})\n        status = res.get('success')\n        result = res.get('result')\n        if not status:\n            return json.dumps({'success':'false', 'action':'create cluster', 'message':result})\n        [status, result] = G_vclustermgr.create_cluster(clustername, user, image, user_info, setting)\n        if status:\n            return json.dumps({'success':'true', 'action':'create cluster', 'message':result})\n        else:\n            post_to_user(\"/user/usageRecover/\", {'token':form.get('token'), 'setting':json.dumps(setting)})\n            return json.dumps({'success':'false', 'action':'create cluster', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/scaleout/\", methods=['POST'])\n@login_required\n@beans_check\ndef scaleout_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    logger.info (\"scaleout: %s\" % form)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        logger.info(\"handle request : scale out %s\" % clustername)\n        image = {}\n        image['name'] = form.get(\"imagename\", None)\n        image['type'] = form.get(\"imagetype\", None)\n        image['owner'] = form.get(\"imageowner\", None)\n        user_info = post_to_user(\"/user/selfQuery/\", {'token':form.get(\"token\")})\n        user_info = json.dumps(user_info)\n        setting = {\n                'cpu': form.get('cpuSetting'),\n                'memory': form.get('memorySetting'),\n                'disk': form.get('diskSetting')\n                }\n        res = post_to_user(\"/user/usageInc/\", {'token':form.get('token'), 'setting':json.dumps(setting)})\n        status = res.get('success')\n        result = res.get('result')\n        if not status:\n            return json.dumps({'success':'false', 'action':'scale out', 'message': result})\n        [status, result] = G_vclustermgr.scale_out_cluster(clustername, user, image, user_info, setting)\n        if status:\n            return json.dumps({'success':'true', 'action':'scale out', 'message':result})\n        else:\n            post_to_user(\"/user/usageRecover/\", {'token':form.get('token'), 'setting':json.dumps(setting)})\n            return json.dumps({'success':'false', 'action':'scale out', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/scalein/\", methods=['POST'])\n@login_required\ndef scalein_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        logger.info(\"handle request : scale in %s\" % clustername)\n        containername = form.get(\"containername\", None)\n        [status, usage_info] = G_vclustermgr.get_clustersetting(clustername, user, containername, False)\n        if status:\n            post_to_user(\"/user/usageRelease/\", {'token':form.get('token'), 'cpu':usage_info['cpu'], 'memory':usage_info['memory'],'disk':usage_info['disk']})\n        [status, result] = G_vclustermgr.scale_in_cluster(clustername, user, containername)\n        if status:\n            return json.dumps({'success':'true', 'action':'scale in', 'message':result})\n        else:\n            return json.dumps({'success':'false', 'action':'scale in', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/start/\", methods=['POST'])\n@login_required\n@beans_check\ndef start_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        user_info = post_to_user(\"/user/selfQuery/\", {'token':form.get(\"token\")})\n        logger.info (\"handle request : start cluster %s\" % clustername)\n        [status, result] = G_vclustermgr.start_cluster(clustername, user, user_info)\n        if status:\n            return json.dumps({'success':'true', 'action':'start cluster', 'message':result})\n        else:\n            return json.dumps({'success':'false', 'action':'start cluster', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/stop/\", methods=['POST'])\n@login_required\ndef stop_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        logger.info (\"handle request : stop cluster %s\" % clustername)\n        [status, result] = G_vclustermgr.stop_cluster(clustername, user)\n        if status:\n            return json.dumps({'success':'true', 'action':'stop cluster', 'message':result})\n        else:\n            return json.dumps({'success':'false', 'action':'stop cluster', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/delete/\", methods=['POST'])\n@login_required\ndef delete_cluster(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    G_ulockmgr.acquire(user)\n    try:\n        logger.info (\"handle request : delete cluster %s\" % clustername)\n        user_info = post_to_user(\"/user/selfQuery/\" , {'token':form.get(\"token\")})\n        user_info = json.dumps(user_info)\n        [status, usage_info] = G_vclustermgr.get_clustersetting(clustername, user, \"all\", True)\n        if status:\n            post_to_user(\"/user/usageRelease/\", {'token':form.get('token'), 'cpu':usage_info['cpu'], 'memory':usage_info['memory'],'disk':usage_info['disk']})\n        [status, result] = G_vclustermgr.delete_cluster(clustername, user, user_info)\n        if status:\n            return json.dumps({'success':'true', 'action':'delete cluster', 'message':result})\n        else:\n            return json.dumps({'success':'false', 'action':'delete cluster', 'message':result})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/info/\", methods=['POST'])\n@login_required\ndef info_cluster(user, beans, form):\n\n    global G_vclustermgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    logger.info (\"handle request : info cluster %s\" % clustername)\n    [status, result] = G_vclustermgr.get_clusterinfo(clustername, user)\n    if status:\n        return json.dumps({'success':'true', 'action':'info cluster', 'message':result})\n    else:\n        return json.dumps({'success':'false', 'action':'info cluster', 'message':result})\n\n@app.route(\"/cluster/list/\", methods=['POST'])\n@login_required\ndef list_cluster(user, beans, form):\n    global G_vclustermgr\n    logger.info (\"handle request : list clusters for %s\" % user)\n    [status, clusterlist] = G_vclustermgr.list_clusters(user)\n    if status:\n        return json.dumps({'success':'true', 'action':'list cluster', 'clusters':clusterlist})\n    else:\n        return json.dumps({'success':'false', 'action':'list cluster', 'message':clusterlist})\n\n@app.route(\"/cluster/stopall/\",methods=['POST'])\n@auth_key_required\ndef stopall_cluster():\n    global G_vclustermgr\n    global G_ulockmgr\n    user = request.form.get('username',None)\n    if user is None:\n        return json.dumps({'success':'false', 'message':'User is required!'})\n    G_ulockmgr.acquire(user)\n    try:\n        logger.info (\"handle request : stop all clusters for %s\" % user)\n        [status, clusterlist] = G_vclustermgr.list_clusters(user)\n        if status:\n            for cluster in clusterlist:\n                G_vclustermgr.stop_cluster(cluster,user)\n            return json.dumps({'success':'true', 'action':'stop all cluster'})\n        else:\n            return json.dumps({'success':'false', 'action':'stop all cluster', 'message':clusterlist})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cluster/flush/\", methods=['POST'])\n@login_required\ndef flush_cluster(user, beans, form):\n    global G_vclustermgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    from_lxc = form.get('from_lxc', None)\n    G_vclustermgr.flush_cluster(user,clustername,from_lxc)\n    return json.dumps({'success':'true', 'action':'flush'})\n\n@app.route(\"/cluster/save/\", methods=['POST'])\n@login_required\ndef save_cluster(user, beans, form):\n    global G_vclustermgr\n    clustername = form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n\n    imagename = form.get(\"image\", None)\n    description = form.get(\"description\", None)\n    containername = form.get(\"containername\", None)\n    isforce = form.get(\"isforce\", None)\n    G_ulockmgr.acquire(user)\n    try:\n        if not isforce == \"true\":\n            [status,message] = G_vclustermgr.image_check(user,imagename)\n            if not status:\n                return json.dumps({'success':'false','reason':'exists', 'message':message})\n\n        user_info = post_to_user(\"/user/selfQuery/\", {'token':form.get(\"token\")})\n        [status,message] = G_vclustermgr.create_image(user,clustername,containername,imagename,description,user_info[\"data\"][\"groupinfo\"][\"image\"])\n        if status:\n            logger.info(\"image has been saved\")\n            return json.dumps({'success':'true', 'action':'save'})\n        else:\n            logger.debug(message)\n            return json.dumps({'success':'false', 'reason':'exceed', 'message':message})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/admin/ulock/release/\", methods=['POST'])\n@login_required\ndef release_ulock(user, beans, form):\n    global G_ulockmgr\n    if user != 'root':\n        return json.dumps({'success':'false', 'message':'root is required.'})\n    release_user = form.get(\"ulockname\",None)\n    if release_user is None:\n        return json.dumps({'success':'false', 'message':'ulockname is required.'})\n\n    try:\n        G_ulockmgr.release(release_user)\n    except Exception as e:\n        logger.error(traceback.format_exc())\n        return json.dumps({'success':'false', 'message':'fail to release lock %s' % release_user})\n    return json.dumps({'success':'true', 'message':'lock %s release successfully' % release_user})\n\n@app.route(\"/admin/migrate_cluster/\", methods=['POST'])\n@auth_key_required\ndef migrate_cluster():\n    global G_vclustermgr\n    global G_ulockmgr\n    user = request.form.get('username',None)\n    if user is None:\n        return json.dumps({'success':'false', 'message':'User is required!'})\n    clustername = request.form.get('clustername', None)\n    if (clustername == None):\n        return json.dumps({'success':'false', 'message':'clustername is null'})\n    new_hosts = request.form.get('new_hosts', None)\n    if (new_hosts == None):\n        return json.dumps({'success':'false', 'message':'new_hosts is null'})\n    new_host_list = new_hosts.split(',')\n    G_ulockmgr.acquire(user)\n    auth_key = env.getenv('AUTH_KEY')\n    try:\n        logger.info (\"handle request : migrate cluster to %s. user:%s clustername:%s\" % (str(new_hosts), user, clustername))\n        res = post_to_user(\"/master/user/groupinfo/\", {'auth_key':auth_key})\n        groups = json.loads(res['groups'])\n        quotas = {}\n        for group in groups:\n            #logger.info(group)\n            quotas[group['name']] = group['quotas']\n        rc_info = post_to_user(\"/master/user/recoverinfo/\", {'username':user,'auth_key':auth_key})\n        groupname = rc_info['groupname']\n        user_info = {\"data\":{\"id\":rc_info['uid'],\"groupinfo\":quotas[groupname]}}\n\n        logger.info(\"Migrate cluster for user(%s) cluster(%s) to new_hosts(%s). user_info(%s)\"\n                    %(clustername, user, str(new_host_list), user_info))\n\n        [status,msg] = G_vclustermgr.migrate_cluster(clustername, user, new_host_list, user_info)\n        if not status:\n            logger.error(msg)\n            return json.dumps({'success':'false', 'message': msg})\n        return json.dumps({'success':'true', 'action':'migrate_container'})\n    except Exception as ex:\n        logger.error(traceback.format_exc())\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/host/migrate/\", methods=['POST'])\n@login_required\ndef migrate_host(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    src_host = request.form.get('src_host', None)\n    dst_host_list = request.form.getlist('dst_host_list', None)\n\n    if src_host is None or dst_host_list is None:\n        return json.dumps({'success':'false', 'message': 'src host or dst host list is null'})\n    [status, msg] = G_vclustermgr.migrate_host(src_host, dst_host_list, G_ulockmgr)\n    if status:\n        return json.dumps({'success': 'true', 'action': 'migrate_host'})\n    else:\n        return json.dumps({'success': 'false', 'message': msg})\n\n\n\n@app.route(\"/image/list/\", methods=['POST'])\n@login_required\ndef list_image(user, beans, form):\n    global G_imagemgr\n    images = G_imagemgr.list_images(user)\n    return json.dumps({'success':'true', 'images': images})\n\n@app.route(\"/image/updatebase/\", methods=['POST'])\n@login_required\ndef update_base(user, beans, form):\n    global G_imagemgr\n    global G_vclustermgr\n    [success, status] = G_imagemgr.update_base_image(user, G_vclustermgr, form.get('image'))\n    return json.dumps({'success':'true', 'message':status})\n\n@app.route(\"/image/description/\", methods=['POST'])\n@login_required\ndef description_image(user, beans, form):\n    global G_imagemgr\n    image = {}\n    image['name'] = form.get(\"imagename\", None)\n    image['type'] = form.get(\"imagetype\", None)\n    image['owner'] = form.get(\"imageowner\", None)\n    description = G_imagemgr.get_image_description(user,image)\n    return json.dumps({'success':'true', 'message':description})\n\n@app.route(\"/image/share/\", methods=['POST'])\n@login_required\ndef share_image(user, beans, form):\n    global G_imagemgr\n    image = form.get('image')\n    G_ulockmgr.acquire(user)\n    try:\n        G_imagemgr.shareImage(user,image)\n        return json.dumps({'success':'true', 'action':'share'})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/image/unshare/\", methods=['POST'])\n@login_required\ndef unshare_image(user, beans, form):\n    global G_imagemgr\n    image = form.get('image', None)\n    G_ulockmgr.acquire(user)\n    try:\n        G_imagemgr.unshareImage(user,image)\n        return json.dumps({'success':'true', 'action':'unshare'})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/image/delete/\", methods=['POST'])\n@login_required\ndef delete_image(user, beans, form):\n    global G_imagemgr\n    image = form.get('image', None)\n    G_ulockmgr.acquire(user)\n    try:\n        G_imagemgr.removeImage(user,image)\n        return json.dumps({'success':'true', 'action':'delete'})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/image/copy/\", methods=['POST'])\n@login_required\ndef copy_image(user, beans, form):\n    global G_imagemgr\n    global G_ulockmgr\n    image = form.get('image', None)\n    target = form.get('target',None)\n    token = form.get('token',None)\n    G_ulockmgr.acquire(user)\n    try:\n        res = G_imagemgr.copyImage(user,image,token,target)\n        return json.dumps(res)\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message': str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/image/copytarget/\", methods=['POST'])\n@login_required\n@auth_key_required\ndef copytarget_image(user, beans, form):\n    global G_imagemgr\n    global G_ulockmgr\n    imagename = form.get('imagename',None)\n    description = form.get('description',None)\n    try:\n        G_ulockmgr.acquire(user)\n        res = G_imagemgr.updateinfo(user,imagename,description)\n        return json.dumps({'success':'true', 'action':'copy image to target.'})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message':str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/cloud/setting/get/\", methods=['POST'])\n@login_required\ndef query_account_cloud(cur_user, user, form):\n    global G_cloudmgr\n    logger.info(\"handle request: cloud/setting/get/\")\n    result = G_cloudmgr.getSettingFile()\n    return json.dumps(result)\n\n@app.route(\"/cloud/setting/modify/\", methods=['POST'])\n@login_required\ndef modify_account_cloud(cur_user, user, form):\n    global G_cloudmgr\n    logger.info(\"handle request: cloud/setting/modify/\")\n    result = G_cloudmgr.modifySettingFile(form.get('setting',None))\n    return json.dumps(result)\n\n@app.route(\"/cloud/node/add/\", methods=['POST'])\n@login_required\ndef add_node_cloud(user, beans, form):\n    global G_cloudmgr\n    logger.info(\"handle request: cloud/node/add/\")\n    G_cloudmgr.engine.addNodeAsync()\n    result = {'success':'true'}\n    return json.dumps(result)\n\n@app.route(\"/addproxy/\", methods=['POST'])\n@login_required\ndef addproxy(user, beans, form):\n    global G_vclustermgr\n    logger.info (\"handle request : add proxy\")\n    proxy_ip = form.get(\"ip\", None)\n    proxy_port = form.get(\"port\", None)\n    clustername = form.get(\"clustername\", None)\n    [status, message] = G_vclustermgr.addproxy(user,clustername,proxy_ip,proxy_port)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'addproxy'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n\n@app.route(\"/deleteproxy/\", methods=['POST'])\n@login_required\ndef deleteproxy(user, beans, form):\n    global G_vclustermgr\n    logger.info (\"handle request : delete proxy\")\n    clustername = form.get(\"clustername\", None)\n    G_vclustermgr.deleteproxy(user,clustername)\n    return json.dumps({'success':'true', 'action':'deleteproxy'})\n\n@app.route(\"/port_mapping/add/\", methods=['POST'])\n@login_required\ndef add_port_mapping(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    logger.info (\"handle request : add port mapping\")\n    node_name = form.get(\"node_name\",None)\n    node_ip = form.get(\"node_ip\", None)\n    node_port = form.get(\"node_port\", None)\n    clustername = form.get(\"clustername\", None)\n    if node_name is None or node_ip is None or node_port is None or clustername is None:\n        return json.dumps({'success':'false', 'message': 'Illegal form.'})\n    user_info = post_to_user(\"/user/selfQuery/\", data = {\"token\": form.get(\"token\")})\n    G_ulockmgr.acquire(user)\n    try:\n        [status, message] = G_vclustermgr.add_port_mapping(user,clustername,node_name,node_ip,node_port,user_info['data']['groupinfo'])\n        if status is True:\n            return json.dumps({'success':'true', 'action':'addproxy'})\n        else:\n            return json.dumps({'success':'false', 'message': message})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message':str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/port_mapping/delete/\", methods=['POST'])\n@login_required\ndef delete_port_mapping(user, beans, form):\n    global G_vclustermgr\n    global G_ulockmgr\n    logger.info (\"handle request : delete port mapping\")\n    node_name = form.get(\"node_name\",None)\n    clustername = form.get(\"clustername\", None)\n    node_port = form.get(\"node_port\", None)\n    if node_name is None or clustername is None:\n        return json.dumps({'success':'false', 'message': 'Illegal form.'})\n    G_ulockmgr.acquire(user)\n    try:\n        [status, message] = G_vclustermgr.delete_port_mapping(user,clustername,node_name,node_port)\n        if status is True:\n            return json.dumps({'success':'true', 'action':'addproxy'})\n        else:\n            return json.dumps({'success':'false', 'message': message})\n    except Exception as ex:\n        logger.error(str(ex))\n        return json.dumps({'success':'false', 'message':str(ex)})\n    finally:\n        G_ulockmgr.release(user)\n\n@app.route(\"/monitor/hosts/<com_id>/<issue>/\", methods=['POST'])\n@login_required\ndef hosts_monitor(user, beans, form, com_id, issue):\n    global G_clustername\n\n    logger.info(\"handle request: monitor/hosts\")\n    res = {}\n    fetcher = monitor.Fetcher(com_id)\n    if issue == 'meminfo':\n        res['meminfo'] = fetcher.get_meminfo()\n    elif issue == 'gpuinfo':\n        res['gpuinfo'] = fetcher.get_gpuinfo()\n    elif issue == 'cpuinfo':\n        res['cpuinfo'] = fetcher.get_cpuinfo()\n    elif issue == 'cpuconfig':\n        res['cpuconfig'] = fetcher.get_cpuconfig()\n    elif issue == 'diskinfo':\n        res['diskinfo'] = fetcher.get_diskinfo()\n    elif issue == 'osinfo':\n        res['osinfo'] = fetcher.get_osinfo()\n    #elif issue == 'concpuinfo':\n     #   res['concpuinfo'] = fetcher.get_concpuinfo()\n    elif issue == 'containers':\n        res['containers'] = fetcher.get_containers()\n    elif issue == 'status':\n        res['status'] = fetcher.get_status()\n    elif issue == 'containerslist':\n        res['containerslist'] = fetcher.get_containerslist()\n    elif issue == 'containersinfo':\n        res = []\n        conlist = fetcher.get_containerslist()\n        for container in conlist:\n            ans = {}\n            confetcher = monitor.Container_Fetcher(etcdaddr,G_clustername)\n            ans = confetcher.get_basic_info(container)\n            ans['cpu_use'] = confetcher.get_cpu_use(container)\n            ans['mem_use'] = confetcher.get_mem_use(container)\n            res.append(ans)\n    else:\n        return json.dumps({'success':'false', 'message':'not supported request'})\n\n    return json.dumps({'success':'true', 'monitor':res})\n\n\n@app.route(\"/monitor/vnodes/<con_id>/<issue>/\", methods=['POST'])\n@login_required\ndef vnodes_monitor(user, beans, form, con_id, issue):\n    global G_clustername\n    global G_historymgr\n    logger.info(\"handle request: monitor/vnodes\")\n    res = {}\n    fetcher = monitor.Container_Fetcher(con_id)\n    if issue == 'info':\n        res = fetcher.get_info()\n    elif issue == 'cpu_use':\n        res['cpu_use'] = fetcher.get_cpu_use()\n    elif issue == 'mem_use':\n        res['mem_use'] = fetcher.get_mem_use()\n    elif issue == 'disk_use':\n        res['disk_use'] = fetcher.get_disk_use()\n    elif issue == 'basic_info':\n        res['basic_info'] = fetcher.get_basic_info()\n    elif issue == 'net_stats':\n        res['net_stats'] = fetcher.get_net_stats()\n    elif issue == 'history':\n        res['history'] = G_historymgr.getHistory(con_id)\n    elif issue == 'owner':\n        names = con_id.split('-')\n        result = post_to_user(\"/user/query/\", data = {\"token\": form.get(token)})\n        if result['success'] == 'false':\n            res['username'] = \"\"\n            res['truename'] = \"\"\n        else:\n            res['username'] = result['data']['username']\n            res['truename'] = result['data']['truename']\n    else:\n        res = \"Unspported Method!\"\n    return json.dumps({'success':'true', 'monitor':res})\n\n\n@app.route(\"/monitor/user/<issue>/\", methods=['POST'])\n@login_required\ndef user_quotainfo_monitor(user, beans, form, issue):\n    global G_historymgr\n    if issue == 'quotainfo':\n        logger.info(\"handle request: monitor/user/quotainfo/\")\n        user_info = post_to_user(\"/user/selfQuery/\", {'token':form.get(\"token\")})\n        quotainfo = user_info['data']['groupinfo']\n        return json.dumps({'success':'true', 'quotainfo':quotainfo})\n    elif issue == 'createdvnodes':\n        logger.info(\"handle request: monitor/user/createdvnodes/\")\n        res = G_historymgr.getCreatedVNodes(user)\n        return json.dumps({'success':'true', 'createdvnodes':res})\n    elif issue == 'net_stats':\n        logger.info(\"handle request: monitor/user/net_stats/\")\n        res = monitor.Container_Fetcher.get_user_net_stats(user)\n        return json.dumps({'success':'true', 'net_stats':res})\n    else:\n        return json.dumps({'success':'false', 'message':\"Unspported Method!\"})\n\n@app.route(\"/monitor/listphynodes/\", methods=['POST'])\n@login_required\ndef listphynodes_monitor(user, beans, form):\n    global G_nodemgr\n    logger.info(\"handle request: monitor/listphynodes/\")\n    res = {}\n    res['allnodes'] = G_nodemgr.get_nodeips()\n    return json.dumps({'success':'true', 'monitor':res})\n\n@app.route(\"/monitor/pending_gpu_tasks/\", methods=['POST'])\n@login_required\ndef pending_gpu_tasks_monitor(user, beans, form):\n    global G_taskmgr\n    logger.info(\"handle request: monitor/pending_gpu_tasks/\")\n    res = {}\n    res['pending_tasks'] = G_taskmgr.get_pending_gpu_tasks_info()\n    return json.dumps({'success':'true', 'monitor':res})\n\n@app.route(\"/billing/beans/\", methods=['POST'])\n@auth_key_required\ndef billing_beans():\n    form = request.form\n    res = post_to_user(\"/billing/beans/\",data=form)\n    logger.info(res)\n    return json.dumps(res)\n\n\n@app.route(\"/system/parmList/\", methods=['POST'])\n@login_required\ndef parmList_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/parmList/\")\n    result = G_sysmgr.getParmList()\n    return json.dumps(result)\n\n@app.route(\"/system/modify/\", methods=['POST'])\n@login_required\ndef modify_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/modify/\")\n    field = form.get(\"field\", None)\n    parm = form.get(\"parm\", None)\n    val = form.get(\"val\", None)\n    [status, message] = G_sysmgr.modify(field,parm,val)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'modify_system'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n    return json.dumps(result)\n\n@app.route(\"/system/clear_history/\", methods=['POST'])\n@login_required\ndef clear_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/clear_history/\")\n    field = form.get(\"field\", None)\n    parm = form.get(\"parm\", None)\n    [status, message] = G_sysmgr.clear(field,parm)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'clear_history'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n    return json.dumps(result)\n\n@app.route(\"/system/add/\", methods=['POST'])\n@login_required\ndef add_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/add/\")\n    field = form.get(\"field\", None)\n    parm = form.get(\"parm\", None)\n    val = form.get(\"val\", None)\n    [status, message] = G_sysmgr.add(field, parm, val)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'add_parameter'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n    return json.dumps(result)\n\n@app.route(\"/system/delete/\", methods=['POST'])\n@login_required\ndef delete_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/delete/\")\n    field = form.get(\"field\", None)\n    parm = form.get(\"parm\", None)\n    [status, message] = G_sysmgr.delete(field,parm)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'delete_parameter'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n    return json.dumps(result)\n\n@app.route(\"/system/reset_all/\", methods=['POST'])\n@login_required\ndef resetall_system(user, beans, form):\n    global G_sysmgr\n    logger.info(\"handle request: system/reset_all/\")\n    field = form.get(\"field\", None)\n    [status, message] = G_sysmgr.reset_all(field)\n    if status is True:\n        return json.dumps({'success':'true', 'action':'reset_all'})\n    else:\n        return json.dumps({'success':'false', 'message': message})\n    return json.dumps(result)\n\n@app.route(\"/batch/job/add/\", methods=['POST'])\n@login_required\n@beans_check\ndef add_job(user,beans,form):\n    global G_jobmgr\n    job_data = form.to_dict()\n    job_info = {\n        'tasks': {}\n    }\n    message = {\n        'success': 'true',\n        'message': 'add batch job success'\n    }\n    for key in job_data:\n        if key == 'csrf_token':\n            continue\n        key_arr = key.split('_')\n        value = job_data[key]\n        if key_arr[0] == 'srcAddr' and value == '':\n            #task_idx = 'task_' + key_arr[1]\n            if task_idx in job_info['tasks']:\n                job_info['tasks'][task_idx]['srcAddr'] = '/root'\n            else:\n                job_info['tasks'][task_idx] = {\n                    'srcAddr': '/root'\n                }\n        elif key_arr[0] != 'dependency'and value == '':\n            message['success'] = 'false'\n            message['message'] = 'value of %s is null' % key\n        elif len(key_arr) == 1:\n            job_info[key_arr[0]] = value\n        elif len(key_arr) == 2:\n            key_prefix, task_idx = key_arr[0], key_arr[1]\n            #task_idx = 'task_' + task_idx\n            if task_idx in job_info[\"tasks\"]:\n                job_info[\"tasks\"][task_idx][key_prefix] = value\n            else:\n                tmp_dict = {\n                    key_prefix: value\n                }\n                job_info[\"tasks\"][task_idx] = tmp_dict\n        elif len(key_arr) == 3:\n            key_prefix, task_idx, mapping_idx = key_arr[0], key_arr[1], key_arr[2]\n            #task_idx = 'task_' + task_idx\n            mapping_idx = 'mapping_' + mapping_idx\n            if task_idx in job_info[\"tasks\"]:\n                if \"mapping\" in job_info[\"tasks\"][task_idx]:\n                    if mapping_idx in job_info[\"tasks\"][task_idx][\"mapping\"]:\n                        job_info[\"tasks\"][task_idx][\"mapping\"][mapping_idx][key_prefix] = value\n                    else:\n                        tmp_dict = {\n                            key_prefix: value\n                        }\n                        job_info[\"tasks\"][task_idx][\"mapping\"][mapping_idx] = tmp_dict\n                else:\n                    job_info[\"tasks\"][task_idx][\"mapping\"] = {\n                        mapping_idx: {\n                            key_prefix: value\n                        }\n                    }\n            else:\n                tmp_dict = {\n                    \"mapping\":{\n                        mapping_idx: {\n                            key_prefix: value\n                        }\n                    }\n                }\n                job_info[\"tasks\"][task_idx] = tmp_dict\n    logger.debug('batch job adding info %s' % json.dumps(job_info, indent=4))\n    [status, msg] = G_jobmgr.add_job(user, job_info)\n    if status:\n        return json.dumps(message)\n    else:\n        logger.debug('fail to add batch job: %s' % msg)\n        message[\"success\"] = \"false\"\n        message[\"message\"] = msg\n        return json.dumps(message)\n    return json.dumps(message)\n\n@app.route(\"/batch/job/list/\", methods=['POST'])\n@login_required\ndef list_job(user,beans,form):\n    global G_jobmgr\n    result = {\n        'success': 'true',\n        'data': G_jobmgr.list_jobs(user)\n    }\n    return json.dumps(result)\n\n@app.route(\"/batch/job/listall/\", methods=['POST'])\n@login_required\ndef list_all_job(user,beans,form):\n    global G_jobmgr\n    result = {\n        'success': 'true',\n        'data': G_jobmgr.list_all_jobs()\n    }\n    return json.dumps(result)\n\n@app.route(\"/batch/job/info/\", methods=['POST'])\n@login_required\ndef info_job(user,beans,form):\n    global G_jobmgr\n    jobid = form.get(\"jobid\",\"\")\n    [success, data] = G_jobmgr.get_job(user, jobid)\n    if success:\n        return json.dumps({'success':'true', 'data':data})\n    else:\n        return json.dumps({'success':'false', 'message': data})\n\n@app.route(\"/batch/job/stop/\", methods=['POST'])\n@login_required\ndef stop_job(user,beans,form):\n    global G_jobmgr\n    jobid = form.get(\"jobid\",\"\")\n    [success,msg] = G_jobmgr.stop_job(user,jobid)\n    if success:\n        return json.dumps({'success':'true', 'action':'stop job'})\n    else:\n        return json.dumps({'success':'false', 'message': msg})\n\n@app.route(\"/batch/job/output/\", methods=['POST'])\n@login_required\ndef get_output(user,beans,form):\n    global G_jobmgr\n    jobid = form.get(\"jobid\",\"\")\n    taskid = form.get(\"taskid\",\"\")\n    vnodeid = form.get(\"vnodeid\",\"\")\n    issue = form.get(\"issue\",\"\")\n    result = {\n        'success': 'true',\n        'data': G_jobmgr.get_output(user,jobid,taskid,vnodeid,issue)\n    }\n    return json.dumps(result)\n\n@app.route(\"/batch/task/info/\", methods=['POST'])\n@login_required\ndef info_task(user,beans,form):\n    pass\n\n@app.route(\"/batch/vnodes/list/\", methods=['POST'])\n@login_required\ndef batch_vnodes_list(user,beans,form):\n    global G_taskmgr\n    result = {\n        'success': 'true',\n        'data': G_taskmgr.get_user_batch_containers(user)\n    }\n    return json.dumps(result)\n\n# @app.route(\"/inside/cluster/scaleout/\", methods=['POST'])\n# @inside_ip_required\n# def inside_cluster_scalout(cur_user, cluster_info, form):\n#     global G_usermgr\n#     global G_vclustermgr\n#     clustername = cluster_info['name']\n#     logger.info(\"handle request : scale out %s\" % clustername)\n#     image = {}\n#     image['name'] = form.get(\"imagename\", None)\n#     image['type'] = form.get(\"imagetype\", None)\n#     image['owner'] = form.get(\"imageowner\", None)\n#     user_info = G_usermgr.selfQuery(cur_user = cur_user)\n#     user = user_info['data']['username']\n#     user_info = json.dumps(user_info)\n#     setting = {\n#             'cpu': form.get('cpuSetting'),\n#             'memory': form.get('memorySetting'),\n#             'disk': form.get('diskSetting')\n#             }\n#     [status, result] = G_usermgr.usageInc(cur_user = cur_user, modification = setting)\n#     if not status:\n#         return json.dumps({'success':'false', 'action':'scale out', 'message': result})\n#     [status, result] = G_vclustermgr.scale_out_cluster(clustername, user, image, user_info, setting)\n#     if status:\n#         return json.dumps({'success':'true', 'action':'scale out', 'message':result})\n#     else:\n#         G_usermgr.usageRecover(cur_user = cur_user, modification = setting)\n#         return json.dumps({'success':'false', 'action':'scale out', 'message':result})\n\n@app.errorhandler(500)\ndef internal_server_error(error):\n    logger.debug(\"An internel server error occured\")\n    logger.error(traceback.format_exc())\n    return json.dumps({'success':'false', 'message':'500 Internal Server Error', 'Unauthorized': 'True'})\n\n\nif __name__ == '__main__':\n    logger.info('Start Flask...:')\n    try:\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/httprest_secret_key.txt')\n        app.secret_key = secret_key_file.read()\n        secret_key_file.close()\n    except:\n        from base64 import b64encode\n        from os import urandom\n        secret_key = urandom(24)\n        secret_key = b64encode(secret_key).decode('utf-8')\n        app.secret_key = secret_key\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/httprest_secret_key.txt', 'w')\n        secret_key_file.write(secret_key)\n        secret_key_file.close()\n\n    os.environ['APP_KEY'] = app.secret_key\n    runcmd = sys.argv[0]\n    app.runpath = runcmd.rsplit('/', 1)[0]\n\n    global G_nodemgr\n    global G_vclustermgr\n    global G_notificationmgr\n    global etcdclient\n    global G_networkmgr\n    global G_clustername\n    global G_sysmgr\n    global G_historymgr\n    global G_applicationmgr\n    global G_ulockmgr\n    global G_cloudmgr\n    global G_jobmgr\n    global G_taskmgr\n    # move 'tools.loadenv' to the beginning of this file\n\n    fs_path = env.getenv(\"FS_PREFIX\")\n    logger.info(\"using FS_PREFIX %s\" % fs_path)\n\n    etcdaddr = env.getenv(\"ETCD\")\n    logger.info(\"using ETCD %s\" % etcdaddr)\n\n    G_clustername = env.getenv(\"CLUSTER_NAME\")\n    logger.info(\"using CLUSTER_NAME %s\" % G_clustername)\n\n    # get network interface\n    net_dev = env.getenv(\"NETWORK_DEVICE\")\n    logger.info(\"using NETWORK_DEVICE %s\" % net_dev)\n\n    ipaddr = network.getip(net_dev)\n    if ipaddr==False:\n        logger.error(\"network device is not correct\")\n        sys.exit(1)\n    else:\n        logger.info(\"using ipaddr %s\" % ipaddr)\n\n    # init etcdlib client\n    try:\n        etcdclient = etcdlib.Client(etcdaddr, prefix = G_clustername)\n    except Exception:\n        logger.error (\"connect etcd failed, maybe etcd address not correct...\")\n        sys.exit(1)\n    mode = 'recovery'\n    if len(sys.argv) > 1 and sys.argv[1] == \"new\":\n        mode = 'new'\n\n    # get public IP and set public Ip in etcd\n    public_IP = env.getenv(\"PUBLIC_IP\")\n    etcdclient.setkey(\"machines/publicIP/\"+ipaddr, public_IP)\n\n    # do some initialization for mode: new/recovery\n    if mode == 'new':\n        # clean and initialize the etcd table\n        if etcdclient.isdir(\"\"):\n            etcdclient.clean()\n        else:\n            etcdclient.createdir(\"\")\n        # token is saved at fs_path/golbal/token\n        token = tools.gen_token()\n        tokenfile = open(fs_path+\"/global/token\", 'w')\n        tokenfile.write(token)\n        tokenfile.write(\"\\n\")\n        tokenfile.close()\n        etcdclient.setkey(\"token\", token)\n        etcdclient.setkey(\"service/master\", ipaddr)\n        etcdclient.setkey(\"service/mode\", mode)\n        etcdclient.createdir(\"machines/allnodes\")\n        etcdclient.createdir(\"machines/runnodes\")\n        etcdclient.setkey(\"vcluster/nextid\", \"1\")\n        # clean all users vclusters files : FS_PREFIX/global/users/<username>/clusters/<clusterid>\n        usersdir = fs_path+\"/global/users/\"\n        for user in os.listdir(usersdir):\n            shutil.rmtree(usersdir+user+\"/clusters\")\n            shutil.rmtree(usersdir+user+\"/hosts\")\n            os.mkdir(usersdir+user+\"/clusters\")\n            os.mkdir(usersdir+user+\"/hosts\")\n    else:\n        # check whether cluster exists\n        if not etcdclient.isdir(\"\")[0]:\n            logger.error (\"cluster not exists, you should use mode:new \")\n            sys.exit(1)\n        # initialize the etcd table for recovery\n        token = tools.gen_token()\n        tokenfile = open(fs_path+\"/global/token\", 'w')\n        tokenfile.write(token)\n        tokenfile.write(\"\\n\")\n        tokenfile.close()\n        etcdclient.setkey(\"token\", token)\n        etcdclient.setkey(\"service/master\", ipaddr)\n        etcdclient.setkey(\"service/mode\", mode)\n        if etcdclient.isdir(\"_lock\")[0]:\n            etcdclient.deldir(\"_lock\")\n\n    #init portcontrol\n    portcontrol.init_new()\n    G_ulockmgr = lockmgr.LockMgr()\n\n    clusternet = env.getenv(\"CLUSTER_NET\")\n    logger.info(\"using CLUSTER_NET %s\" % clusternet)\n\n    G_sysmgr = sysmgr.SystemManager()\n\n    G_networkmgr = network.NetworkMgr(clusternet, etcdclient, mode, ipaddr)\n    G_networkmgr.printpools()\n\n    G_cloudmgr = cloudmgr.CloudMgr()\n\n    # start NodeMgr and NodeMgr will wait for all nodes to start ...\n    G_nodemgr = nodemgr.NodeMgr(G_networkmgr, etcdclient, addr = ipaddr, mode=mode)\n    logger.info(\"nodemgr started\")\n    distributedgw = env.getenv(\"DISTRIBUTED_GATEWAY\")\n    G_vclustermgr = vclustermgr.VclusterMgr(G_nodemgr, G_networkmgr, etcdclient, ipaddr, mode, distributedgw)\n    logger.info(\"vclustermgr started\")\n    G_imagemgr = imagemgr.ImageMgr()\n    logger.info(\"imagemgr started\")\n\n    G_releasemgr = releasemgr.ReleaseMgr(G_vclustermgr,G_ulockmgr)\n    G_releasemgr.start()\n    logger.info(\"releasemgr started\")\n\n    logger.info(\"startting to listen on: \")\n    masterip = env.getenv('MASTER_IP')\n    logger.info(\"using MASTER_IP %s\", masterip)\n\n    masterport = env.getenv('MASTER_PORT')\n    logger.info(\"using MASTER_PORT %d\", int(masterport))\n\n    G_historymgr = History_Manager()\n    master_collector = monitor.Master_Collector(G_nodemgr,ipaddr+\":\"+str(masterport))\n    master_collector.start()\n    logger.info(\"master_collector started\")\n    # server = http.server.HTTPServer((masterip, masterport), DockletHttpHandler)\n    logger.info(\"starting master server\")\n\n    G_taskmgr = taskmgr.TaskMgr(G_nodemgr, monitor.Fetcher, ipaddr)\n    G_jobmgr = jobmgr.JobMgr(G_taskmgr)\n    G_taskmgr.set_jobmgr(G_jobmgr)\n    G_taskmgr.start()\n\n    app.run(host = masterip, port = masterport, threaded=True)\n"
  },
  {
    "path": "src/master/jobmgr.py",
    "content": "import time, threading, random, string, os, traceback, requests\nimport master.monitor\nimport subprocess,json\nfrom functools import wraps\nfrom datetime import datetime\n\nfrom utils.log import initlogging, logger\nfrom utils.model import db, Batchjob, Batchtask\nfrom utils import env\n\ndef db_commit():\n    try:\n        db.session.commit()\n    except Exception as err:\n        db.session.rollback()\n        logger.error(traceback.format_exc())\n        raise\n\nclass BatchJob(object):\n    def __init__(self, jobid, user, job_info, old_job_db=None):\n        if old_job_db is None:\n            self.job_db = Batchjob(jobid,user,job_info['jobName'],int(job_info['jobPriority']))\n        else:\n            self.job_db = old_job_db\n            self.job_db.clear()\n            job_info = {}\n            job_info['jobName'] = self.job_db.name\n            job_info['jobPriority'] = self.job_db.priority\n            all_tasks = self.job_db.tasks.all()\n            job_info['tasks'] = {}\n            for t in all_tasks:\n                job_info['tasks'][t.idx] = json.loads(t.config)\n        self.user = user\n        #self.raw_job_info = job_info\n        self.job_id = jobid\n        self.job_name = job_info['jobName']\n        self.job_priority = int(job_info['jobPriority'])\n        self.lock = threading.Lock()\n        self.tasks = {}\n        self.dependency_out = {}\n        self.tasks_cnt = {'pending':0, 'scheduling':0, 'running':0, 'retrying':0, 'failed':0, 'finished':0, 'stopped':0}\n\n        #init self.tasks & self.dependency_out & self.tasks_cnt\n        logger.debug(\"Init BatchJob user:%s job_name:%s create_time:%s\" % (self.job_db.username, self.job_db.name, str(self.job_db.create_time)))\n        raw_tasks = job_info[\"tasks\"]\n        self.tasks_cnt['pending'] = len(raw_tasks.keys())\n        for task_idx in raw_tasks.keys():\n            task_info = raw_tasks[task_idx]\n            if old_job_db is None:\n                task_db = Batchtask(jobid+\"_\"+task_idx, task_idx, task_info)\n                self.job_db.tasks.append(task_db)\n            else:\n                task_db = Batchtask.query.get(jobid+\"_\"+task_idx)\n                task_db.clear()\n            self.tasks[task_idx] = {}\n            self.tasks[task_idx]['id'] = jobid+\"_\"+task_idx\n            self.tasks[task_idx]['config'] = task_info\n            self.tasks[task_idx]['db'] = task_db\n            self.tasks[task_idx]['status'] = 'pending'\n            self.tasks[task_idx]['dependency'] = []\n            dependency = task_info['dependency'].strip().replace(' ', '').split(',')\n            if len(dependency) == 1 and dependency[0] == '':\n                continue\n            for d in dependency:\n                if not d in raw_tasks.keys():\n                    raise ValueError('task %s is not defined in the dependency of task %s' % (d, task_idx))\n                self.tasks[task_idx]['dependency'].append(d)\n                if not d in self.dependency_out.keys():\n                    self.dependency_out[d] = []\n                self.dependency_out[d].append(task_idx)\n\n        if old_job_db is None:\n            db.session.add(self.job_db)\n        db_commit()\n\n        self.log_status()\n        logger.debug(\"BatchJob(id:%s) dependency_out: %s\" % (self.job_db.id, json.dumps(self.dependency_out, indent=3)))\n\n    def data_lock(f):\n        @wraps(f)\n        def new_f(self, *args, **kwargs):\n            self.lock.acquire()\n            try:\n                result = f(self, *args, **kwargs)\n            except Exception as err:\n                self.lock.release()\n                raise err\n            self.lock.release()\n            return result\n        return new_f\n\n    # return the tasks without dependencies\n    @data_lock\n    def get_tasks_no_dependency(self,update_status=False):\n        logger.debug(\"Get tasks without dependencies of BatchJob(id:%s)\" % self.job_db.id)\n        ret_tasks = []\n        for task_idx in self.tasks.keys():\n            if (self.tasks[task_idx]['status'] == 'pending' and\n                len(self.tasks[task_idx]['dependency']) == 0):\n                if update_status:\n                    self.tasks_cnt['pending'] -= 1\n                    self.tasks_cnt['scheduling'] += 1\n                    self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n                    self.tasks[task_idx]['db'].status = 'scheduling'\n                    self.tasks[task_idx]['status'] = 'scheduling'\n                task_name = self.tasks[task_idx]['db'].id\n                ret_tasks.append([task_name, self.tasks[task_idx]['config'], self.job_priority])\n        self.log_status()\n        db_commit()\n        return ret_tasks\n\n    @data_lock\n    def stop_job(self):\n        self.job_db = Batchjob.query.get(self.job_id)\n        self.job_db.status = 'stopping'\n        db_commit()\n\n    # update status of this job based\n    def _update_job_status(self):\n        allcnt = len(self.tasks.keys())\n        if self.tasks_cnt['failed'] != 0:\n            self.job_db.status = 'failed'\n            self.job_db.end_time = datetime.now()\n        elif self.tasks_cnt['finished'] == allcnt:\n            self.job_db.status = 'done'\n            self.job_db.end_time = datetime.now()\n        elif self.job_db.status == 'stopping':\n            if self.tasks_cnt['running'] == 0 and self.tasks_cnt['scheduling'] == 0 and self.tasks_cnt['retrying'] == 0:\n                self.job_db.status = 'stopped'\n                self.job_db.end_time = datetime.now()\n        elif self.tasks_cnt['running'] != 0 or self.tasks_cnt['retrying'] != 0:\n            self.job_db.status = 'running'\n        else:\n            self.job_db.status = 'pending'\n        db_commit()\n\n    # start run a task, update status\n    @data_lock\n    def update_task_running(self, task_idx):\n        logger.debug(\"Update status of task(idx:%s) of BatchJob(id:%s) running.\" % (task_idx, self.job_id))\n        old_status = self.tasks[task_idx]['status']\n        if old_status == 'stopping':\n            logger.info(\"Task(idx:%s) of BatchJob(id:%s) has been stopped.\"% (task_idx, self.job_id))\n            return\n        self.tasks_cnt[old_status] -= 1\n        self.tasks[task_idx]['status'] = 'running'\n        self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n        self.tasks[task_idx]['db'].status = 'running'\n        self.tasks[task_idx]['db'].start_time = datetime.now()\n        self.tasks_cnt['running'] += 1\n        self.job_db = Batchjob.query.get(self.job_id)\n        self._update_job_status()\n        self.log_status()\n\n    # a task has finished, update dependency and return tasks without dependencies\n    @data_lock\n    def finish_task(self, task_idx, running_time, billing):\n        if task_idx not in self.tasks.keys():\n            logger.error('Task_idx %s not in job. user:%s job_name:%s job_id:%s'%(task_idx, self.user, self.job_name, self.job_id))\n            return []\n        logger.debug(\"Task(idx:%s) of BatchJob(id:%s) has finished(running_time=%d,billing=%d). Update dependency...\" % (task_idx, self.job_id, running_time, billing))\n        old_status = self.tasks[task_idx]['status']\n        if old_status == 'stopping':\n            logger.info(\"Task(idx:%s) of BatchJob(id:%s) has been stopped.\"% (task_idx, self.job_id))\n            return\n        self.tasks_cnt[old_status] -= 1\n        self.tasks[task_idx]['status'] = 'finished'\n        self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n        self.tasks[task_idx]['db'].status = 'finished'\n        self.tasks[task_idx]['db'].tried_times += 1\n        self.tasks[task_idx]['db'].running_time = running_time\n        self.tasks[task_idx]['db'].end_time = datetime.now()\n        self.tasks[task_idx]['db'].billing = billing\n        self.tasks[task_idx]['db'].failed_reason = \"\"\n        self.job_db = Batchjob.query.get(self.job_id)\n        self.job_db.billing += billing\n        self.tasks_cnt['finished'] += 1\n\n        if task_idx not in self.dependency_out.keys():\n            self._update_job_status()\n            self.log_status()\n            return []\n        ret_tasks = []\n        for out_idx in self.dependency_out[task_idx]:\n            try:\n                self.tasks[out_idx]['dependency'].remove(task_idx)\n            except Exception as err:\n                logger.warning(traceback.format_exc())\n                continue\n            if (self.tasks[out_idx]['status'] == 'pending' and\n                len(self.tasks[out_idx]['dependency']) == 0):\n                self.tasks_cnt['pending'] -= 1\n                self.tasks_cnt['scheduling'] += 1\n                self.tasks[out_idx]['status'] = 'scheduling'\n                self.tasks[out_idx]['db'] = Batchtask.query.get(self.tasks[out_idx]['id'])\n                self.tasks[out_idx]['db'].status = 'scheduling'\n                task_name = self.job_id + '_' + out_idx\n                ret_tasks.append([task_name, self.tasks[out_idx]['config'], self.job_priority])\n        self._update_job_status()\n        self.log_status()\n        return ret_tasks\n\n    # update retrying status of task\n    @data_lock\n    def update_task_retrying(self, task_idx, reason, tried_times):\n        logger.debug(\"Update status of task(idx:%s) of BatchJob(id:%s) retrying. reason:%s tried_times:%d\" % (task_idx, self.job_id, reason, int(tried_times)))\n        old_status = self.tasks[task_idx]['status']\n        if old_status == 'stopping':\n            logger.info(\"Task(idx:%s) of BatchJob(id:%s) has been stopped.\"% (task_idx, self.job_id))\n            return\n        self.tasks_cnt[old_status] -= 1\n        self.tasks_cnt['retrying'] += 1\n        self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n        self.tasks[task_idx]['db'].status = 'retrying'\n        self.tasks[task_idx]['db'].failed_reason = reason\n        self.tasks[task_idx]['db'].tried_times += 1\n        self.tasks[task_idx]['status'] = 'retrying'\n        self.job_db = Batchjob.query.get(self.job_id)\n        self._update_job_status()\n        self.log_status()\n\n    # update failed status of task\n    @data_lock\n    def update_task_failed(self, task_idx, reason, tried_times, running_time, billing):\n        logger.debug(\"Update status of task(idx:%s) of BatchJob(id:%s) failed. reason:%s tried_times:%d\" % (task_idx, self.job_id, reason, int(tried_times)))\n        old_status = self.tasks[task_idx]['status']\n        self.tasks_cnt[old_status] -= 1\n        self.tasks_cnt['failed'] += 1\n        self.tasks[task_idx]['status'] = 'failed'\n        self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n        self.tasks[task_idx]['db'].status = 'failed'\n        self.tasks[task_idx]['db'].failed_reason = reason\n        self.tasks[task_idx]['db'].tried_times += 1\n        self.tasks[task_idx]['db'].end_time = datetime.now()\n        self.tasks[task_idx]['db'].running_time = running_time\n        self.tasks[task_idx]['db'].billing = billing\n        self.job_db = Batchjob.query.get(self.job_id)\n        self.job_db.billing += billing\n        self._update_job_status()\n        self.log_status()\n\n    @data_lock\n    def update_task_stopped(self, task_idx, running_time, billing):\n        logger.debug(\"Update status of task(idx:%s) of BatchJob(id:%s) stopped.running_time:%d billing:%d\" % (task_idx, self.job_id, int(running_time), billing))\n        old_status = self.tasks[task_idx]['status']\n        if old_status == 'failed' or old_status == 'finished' or old_status == 'stopped':\n            logger.info(\"task(idx:%s) of BatchJob(id:%s) has been done.\"%(task_idx, self.job_id))\n            return False\n        self.tasks_cnt[old_status] -= 1\n        self.tasks_cnt['stopped'] += 1\n        self.tasks[task_idx]['status'] = 'stopped'\n        self.tasks[task_idx]['db'] = Batchtask.query.get(self.tasks[task_idx]['id'])\n        self.tasks[task_idx]['db'].status = 'stopped'\n        self.tasks[task_idx]['db'].end_time = datetime.now()\n        self.tasks[task_idx]['db'].running_time = running_time\n        self.tasks[task_idx]['db'].billing = billing\n        self.job_db = Batchjob.query.get(self.job_id)\n        self.job_db.billing += billing\n        self._update_job_status()\n        self.log_status()\n        return True\n\n    # print status for debuging\n    def log_status(self):\n        task_copy = {}\n        for task_idx in self.tasks.keys():\n            task_copy[task_idx] = {}\n            task_copy[task_idx]['status'] = self.tasks[task_idx]['status']\n            task_copy[task_idx]['dependency'] = self.tasks[task_idx]['dependency']\n        logger.debug(\"BatchJob(id:%s) tasks status: %s\" % (self.job_id, json.dumps(task_copy, indent=3)))\n        logger.debug(\"BatchJob(id:%s)  tasks_cnt: %s\" % (self.job_id, self.tasks_cnt))\n        logger.debug(\"BatchJob(id:%s)  job_status: %s\" %(self.job_id, self.job_db.status))\n\n\nclass JobMgr():\n    # load job information from etcd\n    # initial a job queue and job schedueler\n    def __init__(self, taskmgr):\n        logger.info(\"Init jobmgr...\")\n        try:\n            Batchjob.query.all()\n        except:\n            db.create_all(bind='__all__')\n        self.job_map = {}\n        self.taskmgr = taskmgr\n        self.fspath = env.getenv('FS_PREFIX')\n        self.lock = threading.Lock()\n        self.userpoint = \"http://\" + env.getenv('USER_IP') + \":\" + str(env.getenv('USER_PORT'))\n        self.auth_key = env.getenv('AUTH_KEY')\n\n        self.recover_jobs()\n\n    def recover_jobs(self):\n        logger.info(\"Rerun the unfailed and unfinished jobs...\")\n        try:\n            rejobs = Batchjob.query.filter(~Batchjob.status.in_(['done','failed','stopped']))\n            rejobs = rejobs.order_by(Batchjob.create_time).all()\n            for rejob in rejobs:\n                logger.info(\"Rerun job: \"+rejob.id)\n                logger.debug(str(rejob))\n                job = BatchJob(rejob.id, rejob.username, None, rejob)\n                self.job_map[job.job_id] = job\n                self.process_job(job)\n        except Exception as err:\n            logger.error(traceback.format_exc())\n\n    def charge_beans(self,username,billing):\n        logger.debug(\"Charge user(%s) for %d beans\"%(username, billing))\n        data = {\"owner_name\":username,\"billing\":billing, \"auth_key\":self.auth_key}\n        url = \"/billing/beans/\"\n        return requests.post(self.userpoint+url,data=data).json()\n\n    def add_lock(f):\n        @wraps(f)\n        def new_f(self, *args, **kwargs):\n            self.lock.acquire()\n            try:\n                result = f(self, *args, **kwargs)\n            except Exception as err:\n                self.lock.release()\n                raise err\n            self.lock.release()\n            return result\n        return new_f\n\n    @add_lock\n    def create_job(self, user, job_info):\n        jobid = self.gen_jobid()\n        job = BatchJob(jobid, user, job_info)\n        return job\n\n    # user: username\n    # job_info: a json string\n    # user submit a new job, add this job to queue and database\n    def add_job(self, user, job_info):\n        try:\n            job = self.create_job(user, job_info)\n            self.job_map[job.job_id] = job\n            self.process_job(job)\n        except ValueError as err:\n            logger.error(err)\n            return [False, err.args[0]]\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            #logger.error(err)\n            return [False, err.args[0]]\n        return [True, \"add batch job success\"]\n\n    # user: username\n    # jobid: the id of job\n    def stop_job(self, user, job_id):\n        logger.info(\"[jobmgr] stop job(id:%s) user(%s)\"%(job_id, user))\n        if job_id not in self.job_map.keys():\n            return [False,\"Job id %s does not exists! Maybe it has been finished.\"%job_id]\n        try:\n            job = self.job_map[job_id]\n            if job.job_db.status == 'done' or job.job_db.status == 'failed':\n                return [True,\"\"]\n            if job.user != user and user != 'root':\n                raise Exception(\"Wrong User.\")\n            for task_idx in job.tasks.keys():\n                taskid = job_id + '_' + task_idx\n                self.taskmgr.lazy_stop_task(taskid)\n            job.stop_job()\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            #logger.error(err)\n            return [False, err.args[0]]\n        return [True,\"\"]\n\n    # user: username\n    # list a user's all job\n    def list_jobs(self,user):\n        alljobs = Batchjob.query.filter_by(username=user).all()\n        res = []\n        for job in alljobs:\n            jobdata = json.loads(str(job))\n            tasks = job.tasks.all()\n            jobdata['tasks'] = [t.idx for t in tasks]\n            tasks_vnodeCount = {}\n            for t in tasks:\n                tasks_vnodeCount[t.idx] = int(json.loads(t.config)['vnodeCount'])\n            jobdata['tasks_vnodeCount'] = tasks_vnodeCount\n            res.append(jobdata)\n        return res\n\n    # list all users' jobs\n    def list_all_jobs(self):\n        alljobs = Batchjob.query.all()\n        res = []\n        for job in alljobs:\n            jobdata = json.loads(str(job))\n            tasks = job.tasks.all()\n            jobdata['tasks'] = [t.idx for t in tasks]\n            tasks_vnodeCount = {}\n            for t in tasks:\n                tasks_vnodeCount[t.idx] = int(json.loads(t.config)['vnodeCount'])\n            jobdata['tasks_vnodeCount'] = tasks_vnodeCount\n            res.append(jobdata)\n        return res\n\n    # user: username\n    # jobid: the id of job\n    # get the information of a job, including the status, json description and other information\n    def get_job(self, user, job_id):\n        job = Batchjob.query.get(job_id)\n        if job is None:\n            return [False, \"Jobid(%s) does not exist.\"%job_id]\n        if job.username != user and user != 'root':\n            return [False, \"Wrong User!\"]\n        jobdata = json.loads(str(job))\n        tasks = job.tasks.order_by(Batchtask.idx).all()\n        tasksdata = [json.loads(str(t)) for t in tasks]\n\n        for i in range(len(tasksdata)):\n            if tasksdata[i]['status'] == 'scheduling':\n                order = self.taskmgr.get_task_order(tasksdata[i]['id'])\n                tasksdata[i]['order'] = order\n        jobdata['tasks'] = tasksdata\n\n        return [True, jobdata]\n\n    # check if a job exists\n    def is_job_exist(self, job_id):\n        return Batchjob.query.get(job_id) != None\n\n    # generate a random job id\n    def gen_jobid(self):\n        datestr = datetime.now().strftime(\"%y%m%d\")\n        job_id = datestr+''.join(random.sample(string.ascii_letters + string.digits, 3))\n        while self.is_job_exist(job_id):\n            job_id = datestr+''.join(random.sample(string.ascii_letters + string.digits, 3))\n        return job_id\n\n    # add tasks into taskmgr's queue\n    def add_task_taskmgr(self, user, tasks):\n        for task_name, task_info, task_priority in tasks:\n            if not task_info:\n                logger.error(\"task_info does not exist! task_name(%s)\" % task_name)\n                return False\n            else:\n                logger.debug(\"Add task(name:%s) with priority(%s) to taskmgr's queue.\" % (task_name, task_priority) )\n                self.taskmgr.add_task(user, task_name, task_info, task_priority)\n        return True\n\n    # to process a job, add tasks without dependencies of the job into taskmgr\n    def process_job(self, job):\n        tasks = job.get_tasks_no_dependency(True)\n        return self.add_task_taskmgr(job.user, tasks)\n\n    # report task status from taskmgr when running, failed and finished\n    # task_name: job_id + '_' + task_idx\n    # status: 'running', 'finished', 'retrying', 'failed', 'stopped'\n    # reason: reason for failure or retrying, such as \"FAILED\", \"TIMEOUT\", \"OUTPUTERROR\"\n    # tried_times: how many times the task has been tried.\n    def report(self, user, task_name, status, reason=\"\", tried_times=1, running_time=0, billing=0):\n        split_task_name = task_name.split('_')\n        if len(split_task_name) != 2:\n            logger.error(\"[jobmgr report]Illegal task_name(%s) report from taskmgr\" % task_name)\n            return\n        if billing > 0 and (status == 'failed' or status == 'finished'):\n            self.charge_beans(user, billing)\n        job_id, task_idx = split_task_name\n        if job_id not in self.job_map.keys():\n            logger.error(\"[jobmgr report]jobid(%s) does not exist. task_name(%s)\" % (job_id,task_name))\n            #update data in db\n            taskdb = Batchtask.query.get(task_name)\n            if (taskdb is None or taskdb.status == 'finished' or\n               taskdb.status == 'failed' or taskdb.status == 'stopped'):\n                return\n            taskdb.status = status\n            if status == 'failed':\n                taskdb.failed_reason = reason\n            if status == 'failed' or status == 'stopped' or status == 'finished':\n                taskdb.end_time = datetime.now()\n            if billing > 0:\n                taskdb.running_time = running_time\n                taskdb.billing = billing\n            db_commit()\n            return\n        job  = self.job_map[job_id]\n        if status == \"running\":\n            #logger.debug(str(job.job_db))\n            job.update_task_running(task_idx)\n            #logger.debug(str(job.job_db))\n        elif status == \"finished\":\n            #logger.debug(str(job.job_db))\n            next_tasks = job.finish_task(task_idx, running_time, billing)\n            ret = self.add_task_taskmgr(user, next_tasks)\n            #logger.debug(str(job.job_db))\n        elif status == \"retrying\":\n            job.update_task_retrying(task_idx, reason, tried_times)\n        elif status == \"failed\":\n            job.update_task_failed(task_idx, reason, tried_times, running_time, billing)\n        elif status == \"stopped\":\n            if job.update_task_stopped(task_idx, running_time, billing) and billing > 0:\n                self.charge_beans(user, billing)\n        if job.job_db.status == 'done' or job.job_db.status == 'failed' or job.job_db.status == 'stopped':\n            del self.job_map[job_id]\n\n    # Get Batch job stdout or stderr from its file\n    def get_output(self, username, jobid, taskid, vnodeid, issue):\n        filename = jobid + \"_\" + taskid + \"_\" + vnodeid + \"_\" + issue + \".txt\"\n        fpath = \"%s/global/users/%s/data/batch_%s/%s\" % (self.fspath,username,jobid,filename)\n        logger.info(\"Get output from:%s\" % fpath)\n        try:\n            ret = subprocess.run('tail -n 100 ' + fpath,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n            if ret.returncode != 0:\n                raise IOError(ret.stdout.decode(encoding=\"utf-8\"))\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            return \"\"\n        else:\n            return ret.stdout.decode(encoding=\"utf-8\")\n"
  },
  {
    "path": "src/master/lockmgr.py",
    "content": "#!/usr/bin/python3\n\n'''\nThis module is the manager of threadings locks.\nA LockMgr manages multiple threadings locks.\n'''\n\nimport threading\n\n\nclass LockMgr:\n\n    def __init__(self):\n        # self.locks will store multiple locks by their names.\n        self.locks = {}\n        # the lock of self.locks, is to ensure that only one thread can update it at the same time\n        self.locks_lock = threading.Lock()\n\n    # acquire a lock by its name\n    def acquire(self, lock_name):\n        self.locks_lock.acquire()\n        if lock_name not in self.locks.keys():\n            self.locks[lock_name] = threading.Lock()\n        self.locks_lock.release()\n        self.locks[lock_name].acquire()\n        return\n\n    # release a lock by its name\n    def release(self, lock_name):\n        if lock_name not in self.locks.keys():\n            return\n        self.locks[lock_name].release()\n        return\n"
  },
  {
    "path": "src/master/monitor.py",
    "content": "import threading, time, traceback\nfrom utils import env\nfrom utils.log import logger\nfrom httplib2 import Http\nfrom urllib.parse import urlencode\n\n# major dict to store the monitoring data\n# only use on Master\n# monitor_hosts: use workers' ip addresses as first key.\n# second key: cpuinfo,diskinfo,meminfo,osinfo,cpuconfig,running,containers,containerslist\n# 1.cpuinfo stores the cpu usages data, and it has keys: user,system,idle,iowait\n# 2.diskinfo stores the disks usages data, and it has keys: device,mountpoint,total,used,free,percent\n# 3.meminfo stores the memory usages data, and it has keys: total,used,free,buffers,cached,percent\n# 4.osinfo stores the information of operating system,\n# and it has keys: platform,system,node,release,version,machine,processor\n# 5.cpuconfig stores the information of processors, and it is a list, each element of list is a dict\n# which stores the information of a processor, each element has key: processor,model name,\n# core id, cpu MHz, cache size, physical id.\n# 6.running indicates the status of worker,and it has two values: True, False.\n# 7.containers store the amount of containers on the worker.\n# 8.containers store a list which consists of the names of containers on the worker.\nmonitor_hosts = {}\n\n# monitor_vnodes: use the owners' names of vnodes(containers) as first key.\n# use the names of vnodes(containers) as second key.\n# third key: cpu_use,mem_use,disk_use,basic_info,quota\n# 1.cpu_use has keys: val,unit,hostpercent\n# 2.mem_use has keys: val,unit,usedp\n# 3.disk_use has keys: device,mountpoint,total,used,free,percent\n# 4.basic_info has keys: Name,State,PID,IP,RunningTime,billing,billing_this_hour\n# 5.quota has keys: cpu,memeory\nmonitor_vnodes = {}\n\n# get owner name of a container\ndef get_owner(container_name):\n    names = container_name.split('-')\n    return names[0]\n\n# the thread to collect data from each worker and store them in monitor_hosts and monitor_vnodes\nclass Master_Collector(threading.Thread):\n\n    def __init__(self,nodemgr,master_ip):\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.nodemgr = nodemgr\n        self.master_ip = master_ip\n        self.net_lastbillings = {}\n        self.bytes_per_beans = 1000000000\n        return\n\n    def net_billings(self, username, now_bytes_total):\n        global monitor_vnodes\n        if not username in self.net_lastbillings.keys():\n            self.net_lastbillings[username] = 0\n        elif int(now_bytes_total/self.bytes_per_beans) < self.net_lastbillings[username]:\n            self.net_lastbillings[username] = 0\n        diff = int(now_bytes_total/self.bytes_per_beans) - self.net_lastbillings[username]\n        if diff > 0:\n            auth_key = env.getenv('AUTH_KEY')\n            data = {\"owner_name\":username,\"billing\":diff, \"auth_key\":auth_key}\n            header = {'Content-Type':'application/x-www-form-urlencoded'}\n            http = Http()\n            [resp,content] = http.request(\"http://\"+self.master_ip+\"/billing/beans/\",\"POST\",urlencode(data),headers = header)\n            logger.info(\"response from master:\"+content.decode('utf-8'))\n        self.net_lastbillings[username] += diff\n        monitor_vnodes[username]['net_stats']['net_billings'] = self.net_lastbillings[username]\n\n    def run(self):\n        global monitor_hosts\n        global monitor_vnodes\n        while not self.thread_stop:\n            for worker in monitor_hosts.keys():\n                monitor_hosts[worker]['running'] = False\n            workers = self.nodemgr.get_nodeips()\n            for worker in workers:\n                try:\n                    ip = worker\n                    workerrpc = self.nodemgr.ip_to_rpc(worker)\n                    # fetch data\n                    info = list(eval(workerrpc.workerFetchInfo(self.master_ip)))\n                    #logger.info(info[0])\n                    # store data in monitor_hosts and monitor_vnodes\n                    monitor_hosts[ip] = info[0]\n                    for container in info[1].keys():\n                        owner = get_owner(container)\n                        if not owner in monitor_vnodes.keys():\n                            monitor_vnodes[owner] = {}\n                        monitor_vnodes[owner][container] = info[1][container]\n                    for user in info[2].keys():\n                        if not user in monitor_vnodes.keys():\n                            continue\n                        else:\n                            monitor_vnodes[user]['net_stats'] = info[2][user]\n                            self.net_billings(user, info[2][user]['bytes_total'])\n                except Exception as err:\n                    logger.warning(traceback.format_exc())\n                    logger.warning(err)\n            time.sleep(2)\n            #logger.info(History.query.all())\n            #logger.info(VNode.query.all())\n        return\n\n    def stop(self):\n        self.thread_stop = True\n        return\n\n# master use this class to fetch specific data of containers(vnodes)\nclass Container_Fetcher:\n    def __init__(self,container_name):\n        self.owner = get_owner(container_name)\n        self.con_id = container_name\n        return\n\n    def get_info(self):\n        res = {}\n        res['cpu_use'] = self.get_cpu_use()\n        res['mem_use'] = self.get_mem_use()\n        res['disk_use'] = self.get_disk_use()\n        res['net_stats'] = self.get_net_stats()\n        res['basic_info'] = self.get_basic_info()\n        return res\n\n    def get_cpu_use(self):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[self.owner][self.con_id]['cpu_use']\n            res['quota'] = monitor_vnodes[self.owner][self.con_id]['quota']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_mem_use(self):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[self.owner][self.con_id]['mem_use']\n            res['quota'] = monitor_vnodes[self.owner][self.con_id]['quota']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_disk_use(self):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[self.owner][self.con_id]['disk_use']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_net_stats(self):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[self.owner][self.con_id]['net_stats']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    # get users' net_stats\n    @staticmethod\n    def get_user_net_stats(owner):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[owner]['net_stats']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_basic_info(self):\n        global monitor_vnodes\n        try:\n            res = monitor_vnodes[self.owner][self.con_id]['basic_info']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n# Master use this class to fetch specific data of physical machines(hosts)\nclass Fetcher:\n\n    def __init__(self,host):\n        global monitor_hosts\n        self.info = monitor_hosts[host]\n        return\n\n    #def get_clcnt(self):\n    #   return DockletMonitor.clcnt\n\n    #def get_nodecnt(self):\n    #   return DockletMonitor.nodecnt\n\n    #def get_meminfo(self):\n    #   return self.get_meminfo_('172.31.0.1')\n\n    def get_meminfo(self):\n        try:\n            res = self.info['meminfo']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_gpuinfo(self):\n        try:\n            res = self.info['gpuinfo']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_cpuinfo(self):\n        try:\n            res = self.info['cpuinfo']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_cpuconfig(self):\n        try:\n            res = self.info['cpuconfig']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_diskinfo(self):\n        try:\n            res = self.info['diskinfo']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_osinfo(self):\n        try:\n            res = self.info['osinfo']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_concpuinfo(self):\n        try:\n            res = self.info['concpupercent']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_containers(self):\n        try:\n            res = self.info['containers']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n\n    def get_status(self):\n        try:\n            isexist = self.info['running']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            isexist = False\n        if(isexist):\n            return 'RUNNING'\n        else:\n            return 'STOPPED'\n\n    def get_containerslist(self):\n        try:\n            res = self.info['containerslist']\n        except Exception as err:\n            logger.warning(traceback.format_exc())\n            logger.warning(err)\n            res = {}\n        return res\n"
  },
  {
    "path": "src/master/network.py",
    "content": "#!/usr/bin/python3\n\nimport json, sys, netifaces, threading, traceback\nfrom utils.nettools import netcontrol,ovscontrol\n\nfrom utils.log import logger\n\n# getip : get ip from network interface\n# ifname : name of network interface\ndef getip(ifname):\n    if ifname not in netifaces.interfaces():\n        return False # No such interface\n    else:\n        addrinfo = netifaces.ifaddresses(ifname)\n        if 2 in addrinfo:\n            return netifaces.ifaddresses(ifname)[2][0]['addr']\n        else:\n            return False # network interface is down\n\ndef ip_to_int(addr):\n    [a, b, c, d] = addr.split('.')\n    return (int(a)<<24) + (int(b)<<16) + (int(c)<<8) + int(d)\n\ndef int_to_ip(num):\n    return str((num>>24)&255)+\".\"+str((num>>16)&255)+\".\"+str((num>>8)&255)+\".\"+str(num&255)\n\n# fix addr with cidr, for example, 172.16.0.10/24 --> 172.16.0.0/24\ndef fix_ip(addr, cidr):\n    return int_to_ip( ip_to_int(addr) & ( (-1) << (32-int(cidr)) ) )\n    #return int_to_ip(ip_to_int(addr) & ( ~( (1<<(32-int(cidr)))-1 ) ) )\n\n# jump to next interval address with cidr\ndef next_interval(addr, cidr):\n    addr = fix_ip(addr, int(cidr))\n    return int_to_ip(ip_to_int(addr)+(1<<(32-int(cidr))))\n\n# jump to before interval address with cidr\ndef before_interval(addr, cidr):\n    addr = fix_ip(addr, int(cidr))\n    addrint = ip_to_int(addr)-(1<<(32-int(cidr)))\n    # addrint maybe negative\n    if addrint < 0:\n        return \"-1.-1.-1.-1\"\n    else:\n        return int_to_ip(addrint)\n\n\n# IntervalPool :  manage network blocks with IP/CIDR\n# Data Structure :\n#       ... ...\n#       cidr=16 : A1, A2, ...      # A1 is an IP, means an interval [A1, A1+2^16-1], equals to A1/16\n#       cidr=17 : B1, B2, ...\n#       ... ...\n# API :\n#       allocate\n#       free\nclass IntervalPool(object):\n    # cidr : 1,2, ..., 32\n    def __init__(self, addr_cidr=None, copy=None):\n        if addr_cidr:\n            self.pool = {}\n            [addr, cidr] = addr_cidr.split('/')\n            cidr = int(cidr)\n            # fix addr with cidr, for example, 172.16.0.10/24 --> 172.16.0.0/24\n            addr = fix_ip(addr, cidr)\n            self.info = addr+\"/\"+str(cidr)\n            # init interval pool\n            #   cidr   : [ addr ]\n            #   cidr+1 : [ ]\n            #   ...\n            #   32     : [ ]\n            self.pool[str(cidr)]=[addr]\n            for i in range(cidr+1, 33):\n                self.pool[str(i)]=[]\n        elif copy:\n            self.info = copy['info']\n            self.pool = copy['pool']\n        else:\n            logger.error(\"IntervalPool init failed with no addr_cidr or center\")\n\n    def __str__(self):\n        return json.dumps({'info':self.info, 'pool':self.pool})\n\n    def printpool(self):\n        cidrs = list(self.pool.keys())\n        # sort with key=int(cidr)\n        cidrs.sort(key=int)\n        for i in cidrs:\n            print (i + \" : \" + str(self.pool[i]))\n\n    # allocate an interval with CIDR\n    def allocate(self, thiscidr):\n        # thiscidr -- cidr for this request\n        # upcidr -- up stream which has interval to allocate\n        thiscidr=int(thiscidr)\n        upcidr = thiscidr\n        # find first cidr who can allocate enough ips\n        while((str(upcidr) in self.pool) and  len(self.pool[str(upcidr)])==0):\n            upcidr = upcidr-1\n        if str(upcidr) not in self.pool:\n            return [False, 'Not Enough to Allocate']\n        # get the block/interval to allocate ips\n        upinterval = self.pool[str(upcidr)][0]\n        self.pool[str(upcidr)].remove(upinterval)\n        # split the upinterval and put the rest intervals back to interval pool\n        for i in range(int(thiscidr), int(upcidr), -1):\n            self.pool[str(i)].append(next_interval(upinterval, i))\n            #self.pool[str(i)].sort(key=ip_to_int)  # cidr between thiscidr and upcidr are null, no need to sort\n        return [True, upinterval]\n\n    # check whether the addr/cidr overlaps the self.pool\n    # for example, addr/cidr=172.16.0.48/29 overlaps self.pool['24']=[172.16.0.0]\n    def overlap(self, addr, cidr):\n        cidr=int(cidr)\n        start_cidr=int(self.info.split('/')[1])\n        # test self.pool[cidr] from first cidr pool to last cidr pool\n        for cur_cidr in range(start_cidr, 33):\n            if not self.pool[str(cur_cidr)]:\n                continue\n            # for every cur_cidr, test every possible element covered by pool[cur_cidr] in range of addr/cidr\n            cur_addr=fix_ip(addr, min(cidr, cur_cidr))\n            last_addr=next_interval(addr, cidr)\n            while(ip_to_int(cur_addr)<ip_to_int(last_addr)):\n                if cur_addr in self.pool[str(cur_cidr)]:\n                    return  True\n                cur_addr=next_interval(cur_addr, cur_cidr)\n        return False\n\n    # whether addr/cidr is in the range of self.pool\n    def inrange(self, addr, cidr):\n         pool_addr,pool_cidr=self.info.split('/')\n         if int(cidr)>=int(pool_cidr) and fix_ip(addr,pool_cidr)==pool_addr:\n             return True\n         else:\n             return False\n\n    # deallocate an interval with IP/CIDR\n    def free(self, addr, cidr):\n        if not self.inrange(addr, cidr):\n            return [False, '%s/%s not in range of %s' % (addr, str(cidr), self.info)]\n        if self.overlap(addr, cidr):\n            return [False, '%s/%s overlaps the center pool:%s' % (addr, str(cidr), self.__str__())]\n        cidr = int(cidr)\n        # cidr not in pool means CIDR out of pool range\n        if str(cidr) not in self.pool:\n            return [False, 'CIDR not in pool']\n        addr = fix_ip(addr, cidr)\n        # merge interval and move to up cidr\n        while(True):\n            # cidr-1 not in pool means current CIDR is the top CIDR\n            if str(cidr-1) not in self.pool:\n                break\n            # if addr can satisfy cidr-1, and next_interval also exist,\n            #           merge addr with next_interval to up cidr (cidr-1)\n            # if addr not satisfy cidr-1, and before_interval exist,\n            #           merge addr with before_interval to up cidr, and interval index is before_interval\n            if addr == fix_ip(addr, cidr-1):\n                if next_interval(addr, cidr) in self.pool[str(cidr)]:\n                    self.pool[str(cidr)].remove(next_interval(addr,cidr))\n                    cidr=cidr-1\n                else:\n                    break\n            else:\n                if before_interval(addr, cidr) in self.pool[str(cidr)]:\n                    addr = before_interval(addr, cidr)\n                    self.pool[str(cidr)].remove(addr)\n                    cidr = cidr - 1\n                else:\n                    break\n        self.pool[str(cidr)].append(addr)\n        # sort interval with key=ip_to_int(IP)\n        self.pool[str(cidr)].sort(key=ip_to_int)\n        return [True, \"Free success\"]\n\n# EnumPool : manage network ips with ip or ip list\n# Data Structure : [ A, B, C, ... X ] , A is a IP address\nclass EnumPool(object):\n    def __init__(self, addr_cidr=None, copy=None):\n        if addr_cidr:\n            self.pool = []\n            [addr, cidr] = addr_cidr.split('/')\n            cidr=int(cidr)\n            addr=fix_ip(addr, cidr)\n            self.info = addr+\"/\"+str(cidr)\n            # init enum pool\n            # first IP is network id, last IP is network broadcast address\n            # first and last IP can not be allocated\n            for i in range(1, pow(2, 32-cidr)-1):\n                self.pool.append(int_to_ip(ip_to_int(addr)+i))\n        elif copy:\n            self.info = copy['info']\n            self.pool = copy['pool']\n        else:\n            logger.error(\"EnumPool init failed with no addr_cidr or copy\")\n\n    def __str__(self):\n        return json.dumps({'info':self.info, 'pool':self.pool})\n\n    def printpool(self):\n        print (str(self.pool))\n\n    def acquire(self, num=1):\n        if num > len(self.pool):\n            return [False, \"No enough IPs: %s\" % self.info]\n        result = []\n        for i in range(0, num):\n            result.append(self.pool.pop())\n        return [True, result]\n\n    def acquire_cidr(self, num=1):\n        [status, result] = self.acquire(int(num))\n        if not status:\n            return [status, result]\n        return [True, list(map(lambda x:x+\"/\"+self.info.split('/')[1], result))]\n\n    def inrange(self, ip):\n        addr = self.info.split('/')[0]\n        addrint = ip_to_int(addr)\n        cidr = int(self.info.split('/')[1])\n        if addrint+1 <= ip_to_int(ip) <= addrint+pow(2, 32-cidr)-2:\n            return True\n        return False\n\n    def release(self, ip_or_ips):\n        if type(ip_or_ips) == str:\n            ips = [ ip_or_ips ]\n        else:\n            ips = ip_or_ips\n        # check whether all IPs are not in the pool but in the range of pool\n        for ip in ips:\n            ip = ip.split('/')[0]\n            if (ip in self.pool) or (not self.inrange(ip)):\n                return [False, 'release IPs failed for ip already existing or ip exceeding the network pool, ips to be released: %s, ip pool is: %s and content is : %s' % (ips, self.info, self.pool)]\n        for ip in ips:\n            # maybe ip is in format IP/CIDR\n            ip = ip.split('/')[0]\n            self.pool.append(ip)\n        return [True, \"release success\"]\n\n# wrap EnumPool with vlanid and gateway\nclass UserPool(EnumPool):\n    def __init__(self, addr_cidr=None, copy=None):\n        if addr_cidr:\n            EnumPool.__init__(self, addr_cidr = addr_cidr)\n            #self.vlanid=vlanid\n            self.pool.sort(key=ip_to_int)\n            self.gateway = self.pool[0]\n            self.pool.remove(self.gateway)\n        elif copy:\n            EnumPool.__init__(self, copy = copy)\n            #self.vlanid = int(copy['vlanid'])\n            self.gateway = copy['gateway']\n        else:\n            logger.error(\"UserPool init failed with no addr_cidr or copy\")\n\n    def get_gateway(self):\n        return self.gateway\n\n    def get_gateway_cidr(self):\n        return self.gateway+\"/\"+self.info.split('/')[1]\n\n    def inrange(self, ip):\n        addr = self.info.split('/')[0]\n        addrint = ip_to_int(addr)\n        cidr = int(self.info.split('/')[1])\n        if addrint+2 <= ip_to_int(ip) <= addrint+pow(2, 32-cidr)-2:\n            return True\n        return False\n\n    def printpool(self):\n        print(\"net info:\"+self.info+\",  gateway:\"+self.gateway)\n        print (str(self.pool))\n\n# NetworkMgr : mange docklet network ip address\n#   center : interval pool to allocate and free network block with IP/CIDR\n#   system : enumeration pool to acquire and release system ip address\n#   users : set of users' enumeration pools to manage users' ip address\nclass NetworkMgr(object):\n    def __init__(self, addr_cidr, etcdclient, mode, masterip):\n        self.etcd = etcdclient\n        self.masterip = masterip\n        self.user_locks = threading.Lock()\n        if mode == 'new':\n            logger.info(\"init network manager with %s\" % addr_cidr)\n            self.center = IntervalPool(addr_cidr=addr_cidr)\n            # allocate a pool for system IPs, use CIDR=27, has 32 IPs\n            syscidr = 27\n            [status, sysaddr] = self.center.allocate(syscidr)\n            if status == False:\n                logger.error (\"allocate system ips in __init__ failed\")\n                sys.exit(1)\n            # maybe for system, the last IP address of CIDR is available\n            # But, EnumPool drop the last IP address in its pool -- it is not important\n            self.system = EnumPool(sysaddr+\"/\"+str(syscidr))\n            self.usrgws = {}\n            self.users = {}\n            #self.vlanids = {}\n            #self.init_vlanids(4095, 60)\n            #self.init_shared_vlanids()\n            self.dump_center()\n            self.dump_system()\n        elif mode == 'recovery':\n            logger.info(\"init network manager from etcd\")\n            self.center = None\n            self.system = None\n            self.usrgws = {}\n            self.users = {}\n            #self.vlanids = {}\n            self.load_center()\n            self.load_system()\n            #self.load_vlanids()\n            #self.load_shared_vlanids()\n        else:\n            logger.error(\"mode: %s not supported\" % mode)\n\n    '''def init_vlanids(self, total, block):\n        self.vlanids['block'] = block\n        self.etcd.setkey(\"network/vlanids/info\", str(total)+\"/\"+str(block))\n        for i in range(1, int((total-1)/block)):\n            self.etcd.setkey(\"network/vlanids/\"+str(i), json.dumps(list(range(1+block*(i-1), block*i+1))))\n        self.vlanids['currentpool'] = list(range(1+block*i, total+1))\n        self.vlanids['currentindex'] = i+1\n        self.etcd.setkey(\"network/vlanids/\"+str(i+1), json.dumps(self.vlanids['currentpool']))\n        self.etcd.setkey(\"network/vlanids/current\", str(i+1))'''\n\n    # Data Structure:\n    # shared_vlanids = [{vlanid = ..., sharenum = ...}, {vlanid = ..., sharenum = ...}, ...]\n    '''def init_shared_vlanids(self, vlannum = 128, sharenum = 128):\n        self.shared_vlanids = []\n        for i in range(vlannum):\n            shared_vlanid = {}\n            [status, shared_vlanid['vlanid']] = self.acquire_vlanid()\n            shared_vlanid['sharenum'] = sharenum\n            self.shared_vlanids.append(shared_vlanid)\n        self.etcd.setkey(\"network/shared_vlanids\", json.dumps(self.shared_vlanids))\n\n\n\n    def load_vlanids(self):\n        [status, info] = self.etcd.getkey(\"network/vlanids/info\")\n        self.vlanids['block'] = int(info.split(\"/\")[1])\n        [status, current] = self.etcd.getkey(\"network/vlanids/current\")\n        self.vlanids['currentindex'] = int(current)\n        if self.vlanids['currentindex'] == 0:\n            self.vlanids['currentpool'] = []\n        else:\n            [status, pool]= self.etcd.getkey(\"network/vlanids/\"+str(self.vlanids['currentindex']))\n            self.vlanids['currentpool'] = json.loads(pool)\n\n    def dump_vlanids(self):\n        if self.vlanids['currentpool'] == []:\n            if self.vlanids['currentindex'] != 0:\n                self.etcd.delkey(\"network/vlanids/\"+str(self.vlanids['currentindex']))\n                self.etcd.setkey(\"network/vlanids/current\", str(self.vlanids['currentindex']-1))\n            else:\n                pass\n        else:\n            self.etcd.setkey(\"network/vlanids/\"+str(self.vlanids['currentindex']), json.dumps(self.vlanids['currentpool']))\n\n    def load_shared_vlanids(self):\n        [status, shared_vlanids] = self.etcd.getkey(\"network/shared_vlanids\")\n        if not status:\n            self.init_shared_vlanids()\n        else:\n            self.shared_vlanids = json.loads(shared_vlanids)\n\n    def dump_shared_vlanids(self):\n        self.etcd.setkey(\"network/shared_vlanids\", json.dumps(self.shared_vlanids))'''\n\n    def load_center(self):\n        [status, centerdata] = self.etcd.getkey(\"network/center\")\n        center = json.loads(centerdata)\n        self.center = IntervalPool(copy = center)\n\n    def dump_center(self):\n        self.etcd.setkey(\"network/center\", json.dumps({'info':self.center.info, 'pool':self.center.pool}))\n\n    def load_system(self):\n        [status, systemdata] = self.etcd.getkey(\"network/system\")\n        system = json.loads(systemdata)\n        self.system = EnumPool(copy=system)\n\n    def dump_system(self):\n        self.etcd.setkey(\"network/system\", json.dumps({'info':self.system.info, 'pool':self.system.pool}))\n\n    def load_user(self, username):\n        [status, userdata] = self.etcd.getkey(\"network/users/\"+username)\n        usercopy = json.loads(userdata)\n        user = UserPool(copy = usercopy)\n        logger.debug(\"load user into dict\")\n        self.users[username] = user\n\n    def dump_user(self, username):\n        logger.debug(\"dump user into etcd\")\n        self.etcd.setkey(\"network/users/\"+username, json.dumps({'info':self.users[username].info, 'gateway':self.users[username].gateway, 'pool':self.users[username].pool}))\n\n    def load_usrgw(self,username):\n        [status, data] = self.etcd.getkey(\"network/usrgws/\"+username)\n        if status:\n            self.usrgws[username] = data\n\n    def dump_usrgw(self, username):\n        self.etcd.setkey(\"network/usrgws/\"+username, self.usrgws[username])\n\n    def printpools(self):\n        print (\"<Center>\")\n        self.center.printpool()\n        print (\"<System>\")\n        self.system.printpool()\n        print (\"<users>\")\n        print (\"    users in users is in etcd, not in memory\")\n        #print (\"<vlanids>\")\n        #print (str(self.vlanids['currentindex'])+\":\"+str(self.vlanids['currentpool']))\n\n    '''def acquire_vlanid(self, isshared = False):\n        if isshared:\n            # only share vlanid of the front entry\n            # if sharenum is reduced to 0, move the front entry to the back\n            # if sharenum is still equal to 0, one round of sharing is complete, start another one\n            if self.shared_vlanids[0]['sharenum'] == 0:\n                self.shared_vlanids.append(self.shared_vlanids.pop(0))\n            if self.shared_vlanids[0]['sharenum'] == 0:\n                logger.info(\"shared vlanids not enough, add user to full vlanids\")\n                for shared_vlanid in self.shared_vlanids:\n                    shared_vlanid['sharenum'] = 128\n            self.shared_vlanids[0]['sharenum'] -= 1\n            self.dump_shared_vlanids()\n            return [True, self.shared_vlanids[0]['vlanid']]\n\n        if self.vlanids['currentpool'] == []:\n            if self.vlanids['currentindex'] == 0:\n                return [False, \"No VLAN IDs\"]\n            else:\n                logger.error(\"vlanids current pool is empty with current index not zero\")\n                return [False, \"internal error\"]\n        vlanid = self.vlanids['currentpool'].pop()\n        self.dump_vlanids()\n        if self.vlanids['currentpool'] == []:\n            self.load_vlanids()\n        return [True, vlanid]\n\n    def release_vlanid(self, vlanid):\n        if len(self.vlanids['currentpool']) == self.vlanids['block']:\n            self.vlanids['currentpool'] = [vlanid]\n            self.vlanids['currentindex'] = self.vanids['currentindex']+1\n            self.dump_vlanids()\n        else:\n            self.vlanids['currentpool'].append(vlanid)\n            self.dump_vlanids()\n        return [True, \"Release VLAN ID success\"]'''\n\n    def has_usrgw(self, username):\n        self.load_usrgw(username)\n        return username in self.usrgws.keys()\n\n    def setup_usrgw(self, input_rate_limit, output_rate_limit, username, uid, nodemgr, workerip=None):\n        if not self.has_user(username):\n            return [False,\"user doesn't exist.\"]\n        self.load_usrgw(username)\n        if username in self.usrgws.keys():\n            return [False,\"user's gateway has been set up.\"]\n        self.load_user(username)\n        usrpools = self.users[username]\n        if(workerip is not None):\n            ip = workerip\n            worker = nodemgr.ip_to_rpc(workerip)\n            logger.info(\"setup gateway for %s with %s on %s\" % (username, usrpools.get_gateway_cidr(), ip))\n            self.usrgws[username] = ip\n            self.dump_usrgw(username)\n            worker.setup_gw('docklet-br-'+str(uid), username, usrpools.get_gateway_cidr(), input_rate_limit, output_rate_limit)\n        else:\n            logger.info(\"setup gateway for %s with %s on master\" % (username, usrpools.get_gateway_cidr() ))\n            self.usrgws[username] = self.masterip\n            self.dump_usrgw(username)\n            netcontrol.setup_gw('docklet-br-'+str(uid), username, usrpools.get_gateway_cidr(), input_rate_limit, output_rate_limit)\n        self.dump_user(username)\n        del self.users[username]\n        return [True, \"set up gateway success\"]\n\n    def add_user(self, username, cidr, isshared = False):\n        logger.info (\"add user %s with cidr=%s\" % (username, str(cidr)))\n        self.user_locks.acquire()\n        try:\n            if self.has_user(username):\n                return [False, \"user already exists in users set\"]\n            [status, result] = self.center.allocate(cidr)\n            self.dump_center()\n            if status == False:\n                return [False, result]\n            '''[status, vlanid] = self.acquire_vlanid(isshared)\n            if status:\n                vlanid = int(vlanid)\n            else:\n                self.center.free(result, cidr)\n                self.dump_center()\n                return [False, vlanid]'''\n            self.users[username] = UserPool(addr_cidr = result+\"/\"+str(cidr))\n            #logger.info(\"setup gateway for %s with %s and vlan=%s\" % (username, self.users[username].get_gateway_cidr(), str(vlanid)))\n            #netcontrol.setup_gw('docklet-br', username, self.users[username].get_gateway_cidr(), str(vlanid))\n            self.dump_user(username)\n            del self.users[username]\n            return [True, 'add user success']\n        except Exception as ex:\n            logger.error(str(ex))\n            return [False, str(ex)]\n        finally:\n            self.user_locks.release()\n\n    def del_usrgwbr(self, username, uid, nodemgr):\n        self.load_usrgw(username)\n        if username not in self.usrgws.keys():\n            return [False, \"user does't have gateway or user doesn't exist.\"]\n        ip = self.usrgws[username]\n        logger.info(\"Delete user %s(%s) gateway on %s\" %(username, str(uid), ip))\n        if ip == self.masterip:\n            netcontrol.del_gw('docklet-br-'+str(uid), username)\n            netcontrol.del_bridge('docklet-br-'+str(uid))\n        else:\n            worker = nodemgr.ip_to_rpc(ip)\n            worker.del_gw('docklet-br-'+str(uid), username)\n            worker.del_bridge('docklet-br-'+str(uid))\n        del self.usrgws[username]\n        self.etcd.delkey(\"network/usrgws/\"+username)\n        return [True, 'delete user\\' gateway success']\n\n    def del_user(self, username):\n        self.user_locks.acquire()\n        try:\n            if not self.has_user(username):\n                return [False, username+\" not in users set\"]\n            self.load_user(username)\n            [addr, cidr] = self.users[username].info.split('/')\n            logger.info (\"delete user %s with cidr=%s\" % (username, int(cidr)))\n            self.center.free(addr, int(cidr))\n            self.dump_center()\n            #if not isshared:\n                #self.release_vlanid(self.users[username].vlanid)\n            #netcontrol.del_gw('docklet-br', username)\n            self.etcd.deldir(\"network/users/\"+username)\n            del self.users[username]\n            return [True, 'delete user success']\n        except Exception as ex:\n            logger.error(traceback.format_exc())\n            return [False, str(ex)]\n        finally:\n            self.user_locks.release()\n\n    def check_usergw(self, input_rate_limit, output_rate_limit, username, uid, nodemgr, distributedgw=False):\n        logger.info(\"Check %s(%s) user gateway.\"%(username, str(uid)))\n        if not self.has_user(username):\n            return [False,\"user doesn't exist.\"]\n        self.load_usrgw(username)\n        if username not in self.usrgws.keys():\n            self.usrgws[username] = self.masterip\n            self.dump_usrgw(username)\n        ip = self.usrgws[username]\n        self.load_user(username)\n        if not distributedgw:\n            if not ip == self.masterip:\n                self.del_usrgwbr(username,uid,nodemgr)\n                self.usrgws[username] = self.masterip\n                self.dump_usrgw(username)\n            netcontrol.check_gw('docklet-br-'+str(uid), username, uid, self.users[username].get_gateway_cidr(), input_rate_limit, output_rate_limit)\n            logger.info(\"recover gw success\")\n        else:\n            worker = nodemgr.ip_to_rpc(ip)\n            nodemgr.call_rpc_function(worker,'check_gw',['docklet-br-'+str(uid), username, uid, self.users[username].get_gateway_cidr(), input_rate_limit, output_rate_limit])\n        del self.users[username]\n        return [True, 'check gw ok']\n\n    def check_usergre(self, username, uid, remote, nodemgr, distributedgw=False):\n        logger.info(\"Check %s(%s) gre from gateway host to %s.\" % (username, str(uid), remote))\n        self.load_usrgw(username)\n        if username not in self.usrgws.keys():\n            return [False, 'user does not exist.']\n        ip = self.usrgws[username]\n        if not distributedgw:\n            if not remote == self.masterip:\n                ovscontrol.add_port_gre_withkey('docklet-br-'+str(uid), 'gre-'+str(uid)+'-'+remote, remote, uid)\n        else:\n            if not remote == ip:\n                worker = nodemgr.ip_to_rpc(ip)\n                nodemgr.call_rpc_function(worker,'add_port_gre_withkey',['docklet-br-'+str(uid), 'gre-'+str(uid)+'-'+remote, remote, uid])\n        return [True, 'check gre ok']\n\n    def has_user(self, username):\n        [status, _value] = self.etcd.getkey(\"network/users/\"+username)\n        return status\n\n    def acquire_userips(self, username, num=1):\n        logger.info (\"acquire user ips of %s\" % (username))\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].acquire(num)\n        self.dump_user(username)\n        del self.users[username]\n        return result\n\n    def acquire_userips_cidr(self, username, num=1):\n        logger.info (\"acquire user ips of %s\" % (username))\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].acquire_cidr(num)\n        self.dump_user(username)\n        del self.users[username]\n\n        return result\n\n    # ip_or_ips : one IP address or a list of IPs\n    def release_userips(self, username, ip_or_ips):\n        logger.info (\"release user ips of %s with ips: %s\" % (username, str(ip_or_ips)))\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].release(ip_or_ips)\n        self.dump_user(username)\n        del self.users[username]\n        return result\n\n    def get_usergw(self, username):\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].get_gateway()\n        self.dump_user(username)\n        del self.users[username]\n        return result\n\n    def get_usergw_cidr(self, username):\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].get_gateway_cidr()\n        self.dump_user(username)\n        del self.users[username]\n        return result\n\n    '''def get_uservlanid(self, username):\n        if not self.has_user(username):\n            return [False, 'username not exists in users set']\n        self.load_user(username)\n        result = self.users[username].vlanid\n        self.dump_user(username)\n        del self.users[username]\n        return result'''\n\n    def acquire_sysips(self, num=1):\n        logger.info (\"acquire system ips\")\n        result = self.system.acquire(num)\n        self.dump_system()\n        return result\n\n    def acquire_sysips_cidr(self, num=1):\n        logger.info (\"acquire system ips\")\n        result = self.system.acquire_cidr(num)\n        self.dump_system()\n        return result\n\n    def release_sysips(self, ip_or_ips):\n        logger.info (\"acquire system ips: %s\" % str(ip_or_ips))\n        result = self.system.release(ip_or_ips)\n        self.dump_system()\n        return result\n"
  },
  {
    "path": "src/master/nodemgr.py",
    "content": "#!/usr/bin/python3\n\nimport threading, random, time, xmlrpc.client, sys\n#import network\nfrom utils.nettools import netcontrol,ovscontrol\nfrom utils.log import logger\nfrom utils import env\nfrom queue import Queue\n\n##########################################\n#                NodeMgr\n# Description : manage the physical nodes\n#               1. list running nodes now\n#               2. update node list when new node joins\n# ETCD table :\n#         machines/allnodes  -- all nodes in docklet, for recovery\n#         machines/runnodes  -- run nodes of this start up\n##############################################\nclass NodeMgr(object):\n    def __init__(self, networkmgr, etcdclient, addr, mode):\n        self.addr = addr\n        logger.info (\"begin initialize on %s\" % self.addr)\n        self.networkmgr = networkmgr\n        self.etcd = etcdclient\n        self.mode = mode\n        self.workerport = env.getenv('WORKER_PORT')\n        self.tasks = {}\n        self.recover_queue = Queue(maxsize=0)\n\n        # delete the existing network\n        logger.info (\"delete the existing network\")\n        [success, bridges] = ovscontrol.list_bridges()\n        if success:\n            for bridge in bridges:\n                if bridge.startswith(\"docklet-br\"):\n                    ovscontrol.del_bridge(bridge)\n        else:\n            logger.error(bridges)\n\n        '''if self.mode == 'new':\n            if netcontrol.bridge_exists('docklet-br'):\n                netcontrol.del_bridge('docklet-br')\n            netcontrol.new_bridge('docklet-br')\n        else:\n            if not netcontrol.bridge_exists('docklet-br'):\n                logger.error(\"docklet-br not found\")\n                sys.exit(1)'''\n\n        # get allnodes\n        self.allnodes = self._nodelist_etcd(\"allnodes\")\n        self.runnodes = []\n        self.batchnodes = []\n        self.allrunnodes = []\n        [status, runlist] = self.etcd.listdir(\"machines/runnodes\")\n        for node in runlist:\n            nodeip = node['key'].rsplit('/',1)[1]\n            if node['value'] == 'ok':\n                logger.info (\"running node %s\" % nodeip)\n                self.runnodes.append(nodeip)\n\n        logger.info (\"all nodes are: %s\" % self.allnodes)\n        logger.info (\"run nodes are: %s\" % self.runnodes)\n\n        # start new thread to watch whether a new node joins\n        logger.info (\"start thread to watch new nodes ...\")\n        self.thread_watchnewnode = threading.Thread(target=self._watchnewnode)\n        self.thread_watchnewnode.start()\n        # wait for all nodes joins\n        # while(True):\n        for i in range(10):\n            allin = True\n            for node in self.allnodes:\n                if node not in self.runnodes:\n                    allin = False\n                    break\n            if allin:\n                logger.info(\"all nodes necessary joins ...\")\n                break\n            time.sleep(1)\n        logger.info (\"run nodes are: %s\" % self.runnodes)\n\n\n    # get nodes list from etcd table\n    def _nodelist_etcd(self, which):\n        if which == \"allnodes\" or which == \"runnodes\":\n            [status, nodeinfo]=self.etcd.listdir(\"machines/\"+which)\n            if status:\n                nodelist = []\n                for node in nodeinfo:\n                    nodelist.append(node[\"key\"].rsplit('/', 1)[1])\n                return nodelist\n        return []\n\n    # thread target : watch whether a new node joins\n    def _watchnewnode(self):\n        while(True):\n            time.sleep(0.1)\n            [status, runlist] = self.etcd.listdir(\"machines/runnodes\")\n            if not status:\n                logger.warning (\"get runnodes list failed from etcd \")\n                continue\n            etcd_runip = []\n            for node in runlist:\n                nodeip = node['key'].rsplit('/',1)[1]\n                if node['value']=='waiting':\n                    #   waiting state can be deleted, there is no use to let master check\n                    # this state because worker will change it and master will not change it now.\n                    # it is only preserved for compatible.\n                    logger.info (\"%s want to joins, call it to init first\" % nodeip)\n                elif node['value']=='work':\n                    logger.info (\"new node %s joins\" % nodeip)\n                    etcd_runip.append(nodeip)\n                    # setup GRE tunnels for new nodes\n                    '''if self.addr == nodeip:\n                        logger.debug (\"worker start on master node. not need to setup GRE\")\n                    else:\n                        logger.debug (\"setup GRE for %s\" % nodeip)\n                        if netcontrol.gre_exists('docklet-br', nodeip):\n                            logger.debug(\"GRE for %s already exists, reuse it\" % nodeip)\n                        else:\n                            netcontrol.setup_gre('docklet-br', nodeip)'''\n                    self.etcd.setkey(\"machines/runnodes/\"+nodeip, \"ok\")\n                    if nodeip not in self.runnodes:\n                        self.runnodes.append(nodeip)\n                        # node not in all node list is a new node.\n                        if nodeip not in self.allnodes:\n                            self.allnodes.append(nodeip)\n                            self.etcd.setkey(\"machines/allnodes/\"+nodeip, \"ok\")\n                        else:\n                            # recover node\n                            self.recover_queue.put(nodeip)\n                            #if nodeip in self.tasks:\n                            #    recover_task = threading.Thread(target = self.recover_node, args=(nodeip,self.tasks[nodeip]))\n                            #    recover_task.start()\n                            #    del self.tasks[nodeip]\n                        logger.debug (\"all nodes are: %s\" % self.allnodes)\n                        logger.debug (\"run nodes are: %s\" % self.runnodes)\n                elif node['value'] == 'ok':\n                    etcd_runip.append(nodeip)\n            new_runnodes = []\n            for nodeip in self.runnodes:\n                if nodeip not in etcd_runip:\n                    logger.info (\"Worker %s is stopped, remove %s:%s from rpc client list\" %\n                        (nodeip, nodeip, self.workerport))\n                    #print(self.runnodes)\n                    #print(etcd_runip)\n                    #print(self.rpcs)\n            self.runnodes = etcd_runip\n            self.batchnodes = self.runnodes.copy()\n            self.allrunnodes = self.runnodes.copy()\n            [status, batchlist] = self.etcd.listdir(\"machines/batchnodes\")\n            if status:\n                for node in batchlist:\n                    nodeip = node['key'].rsplit('/', 1)[1]\n                    self.batchnodes.append(nodeip)\n                    self.allrunnodes.append(nodeip)\n\n    def recover_node(self,ip,tasks):\n        logger.info(\"now recover for worker:%s\" % ip)\n        worker = self.ip_to_rpc(ip)\n        for task in tasks:\n            taskname = task['taskname']\n            taskargs = task['args']\n            logger.info(\"recover task:%s in worker:%s\" % (taskname, ip))\n            eval('worker.'+taskname)(*taskargs)\n\n    # get all run nodes' IP addr\n    def get_nodeips(self):\n        return self.allrunnodes\n    \n    def get_batch_nodeips(self):\n        return self.batchnodes\n\n    def get_base_nodeips(self):\n        return self.runnodes\n\n    def get_allnodes(self):\n        return self.allnodes\n\n    def ip_to_rpc(self,ip):\n        if ip in self.allrunnodes:\n            return xmlrpc.client.ServerProxy(\"http://%s:%s\" % (ip, env.getenv(\"WORKER_PORT\")))\n        else:\n            return None\n            #logger.info('Worker %s is not connected, create rpc client failed, push task into queue')\n            #if not ip in self.tasks:\n            #    self.tasks[ip] = []\n            #return self.tasks[ip]\n\n    def call_rpc_function(self, worker, function, args):\n        #if type(worker) is list:\n        #    worker.append({'taskname':function,'args':args})\n        #    return [True, 'append task success']\n        #else:\n        if worker is None:\n            logger.error(\"worker is None, fail to call rpc function.\")\n            return None\n        else:\n            return eval('worker.'+function)(*args)\n"
  },
  {
    "path": "src/master/notificationmgr.py",
    "content": "import json\n\nfrom utils.log import logger\nfrom utils.model import db, Notification, NotificationGroups, User, UserNotificationPair\nfrom master.userManager import administration_required, token_required\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header\nfrom datetime import datetime\nfrom utils import env\nfrom master.settings import settings\n\nclass NotificationMgr:\n    def __init__(self):\n        logger.info(\"Notification Manager init...\")\n        try:\n            Notification.query.all()\n        except:\n            db.create_all()\n        try:\n            NotificationGroups.query.all()\n        except:\n            db.create_all()\n        try:\n            UserNotificationPair.query.all()\n        except:\n            db.create_all()\n        logger.info(\"Notification Manager init done!\")\n\n    def query_user_notifications(self, user):\n        group_name = user.user_group\n        notifies = NotificationGroups.query.filter_by(group_name=group_name).all()\n        notifies.extend(NotificationGroups.query.filter_by(group_name='all').all())\n        notify_ids = [notify.notification_id for notify in notifies]\n        notify_ids = sorted(list(set(notify_ids)), reverse=True)\n        return [Notification.query.filter_by(id=notify_id).first() for notify_id in notify_ids]\n\n    def mail_notification(self, notify_id):\n        email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n        if (email_from_address in ['\\'\\'', '\\\"\\\"', '']):\n            return {'success' : 'true'}\n        notify = Notification.query.filter_by(id=notify_id).first()\n        notify_groups = NotificationGroups.query.filter_by(notification_id=notify_id).all()\n        to_addr = []\n        groups = []\n        for group in notify_groups:\n            groups.append(group.group_name)\n        if 'all' in groups:\n            users = User.query.all()\n            for user in users:\n                to_addr.append(user.e_mail)\n        else:\n            for group in notify_groups:\n                users = User.query.filter_by(user_group=group.group_name).all()\n                for user in users:\n                    to_addr.append(user.e_mail)\n\n        content = notify.content\n        text = '<html><h4>Dear '+ 'user' + ':</h4>' #user.username + ':</h4>'\n        text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Your account in <a href='%s'>%s</a> has been recieved a notification:</p>\n                   <p>%s</p>\n                   <br>\n                   <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Note: DO NOT reply to this email!</p>\n                   <br><br>\n                   <p> <a href='http://docklet.unias.org'>Docklet Team</a>, SEI, PKU</p>\n                ''' % (env.getenv(\"PORTAL_URL\"), env.getenv(\"PORTAL_URL\"), content)\n        text += '<p>'+  str(datetime.utcnow()) + '</p>'\n        text += '</html>'\n        subject = 'Docklet Notification: ' + notify.title\n        msg = MIMEMultipart()\n        textmsg = MIMEText(text,'html','utf-8')\n        msg['Subject'] = Header(subject, 'utf-8')\n        msg['From'] = email_from_address\n        msg.attach(textmsg)\n        s = smtplib.SMTP()\n        s.connect()\n        for address in to_addr:\n            try:\n                msg['To'] = address\n                s.sendmail(email_from_address, address, msg.as_string())\n            except Exception as e:\n                logger.error(e)\n        s.close()\n        return {\"success\": 'true'}\n\n    @administration_required\n    def create_notification(self, *args, **kwargs):\n        '''\n        Usage: createNotification(cur_user = 'Your current user', form = 'Post form')\n        Post form: {title: 'Your title', content: 'Your content', groups: \"['groupA', 'groupB']\"}\n        '''\n        form = kwargs['form']\n        notify = Notification(form['title'], form['content'])\n        group_names = form.getlist('groups')\n        db.session.add(notify)\n        db.session.commit()\n        # groups = json.loads(form['groups'])\n        # for group_name in groups:\n        if 'all' in group_names:\n            group_names = ['all']\n        for group_name in group_names:\n            if group_name == 'none':\n                continue\n            notify_groups = NotificationGroups(notify.id, group_name)\n            db.session.add(notify_groups)\n        db.session.commit()\n        if 'sendMail' in form:\n            self.mail_notification(notify.id)\n        users = User.query.all()\n        for user in users:\n            user_group = user.user_group\n            for group_name in group_names:\n                if user_group == group_name:\n                    tempPair = UserNotificationPair(user.username, notify.id)\n                    db.session.add(tempPair)\n                    break;\n        db.session.commit()\n\n        return {\"success\": 'true'}\n\n    @administration_required\n    def list_notifications(self, *args, **kwargs):\n        notifies = Notification.query.all()\n        notify_infos = []\n        for notify in notifies:\n            if notify is None or notify.status == 'deleted':\n                continue\n            groups = NotificationGroups.query.filter_by(notification_id=notify.id).all()\n            notify_infos.append({\n                'id': notify.id,\n                'title': notify.title,\n                'content': notify.content,\n                'create_date': notify.create_date,\n                'status': notify.status,\n                'groups': [group.group_name for group in groups]\n            })\n        notify_infos.reverse()\n        return {'success': 'true', 'data': notify_infos}\n\n    @administration_required\n    def modify_notification(self, *args, **kwargs):\n        form = kwargs['form']\n        notify_id = form['notify_id']\n        notify = Notification.query.filter_by(id=notify_id).first()\n        notify.title = form['title']\n        notify.content = form['content']\n        notify.status = form['status']\n        notifies_groups = NotificationGroups.query.filter_by(notification_id=notify_id).all()\n        for notify_groups in notifies_groups:\n            db.session.delete(notify_groups)\n        group_names = form.getlist('groups')\n        if 'all' in group_names:\n            group_names = ['all']\n        for group_name in group_names:\n            if group_name == 'none':\n                continue\n            notify_groups = NotificationGroups(notify.id, group_name)\n            db.session.add(notify_groups)\n        db.session.commit()\n        if 'sendMail' in form:\n            self.mail_notification(notify_id)\n        return {\"success\": 'true'}\n\n    @administration_required\n    def delete_notification(self, *args, **kwargs):\n        form = kwargs['form']\n        notify_id = form['notify_id']\n        notify = Notification.query.filter_by(id=notify_id).first()\n        # notify.status = 'deleted'\n        notifies_groups = NotificationGroups.query.filter_by(notification_id=notify_id).all()\n        for notify_groups in notifies_groups:\n            db.session.delete(notify_groups)\n        db.session.delete(notify)\n        db.session.commit()\n        temppairs = UserNotificationPair.query.filter_by(notifyId=notify_id).all()\n        for temppair in temppairs:\n            db.session.delete(temppair)\n        db.session.commit()\n        return {\"success\": 'true'}\n\n    @token_required\n    def query_self_notification_simple_infos(self, *args, **kwargs):\n        user = kwargs['cur_user']\n        username = user.username\n        notifies = self.query_user_notifications(user)\n        notify_simple_infos = []\n        for notify in notifies:\n            if notify is None or notify.status != 'open':\n                continue\n            notifyid = notify.id\n            temppair = UserNotificationPair.query.filter_by(userName=username, notifyId=notifyid).first()\n            if temppair == None:\n                isRead = 0\n                temppair = UserNotificationPair(username, notifyid)\n                db.session.add(temppair)\n                db.session.commit()\n            else:\n                isRead = temppair.isRead\n            notify_simple_infos.append({\n                'id': notify.id,\n                'title': notify.title,\n                'create_date': notify.create_date,\n                'isRead': isRead\n            })\n        return {'success': 'true', 'data': notify_simple_infos}\n\n    @token_required\n    def query_self_notifications_infos(self, *args, **kwargs):\n        user = kwargs['cur_user']\n        username = user.username\n        notifies = self.query_user_notifications(user)\n        notify_infos = []\n        for notify in notifies:\n            if notify is None or notify.status != 'open':\n                continue\n            notifyid = notify.id\n            temppair = UserNotificationPair.query.filter_by(userName=username, notifyId=notifyid).first()\n            if temppair == None:\n                temppair = UserNotificationPair(username, notifyid)\n                db.session.add(temppair)\n            isRead = 1\n            temppair.isRead = 1\n            db.session.add(temppair)\n            db.session.commit()\n            notify_infos.append({\n                'id': notify.id,\n                'title': notify.title,\n                'content': notify.content,\n                'create_date': notify.create_date,\n                'isRead': isRead\n            })\n        return {'success': 'true', 'data': notify_infos}\n\n    @token_required\n    def query_notification(self, *args, **kwargs):\n        user = kwargs['cur_user']\n        form = kwargs['form']\n        group_name = user.user_group\n        notify_id = form['notify_id']\n        groups = NotificationGroups.query.filter_by(notification_id=notify_id).all()\n        if not(group_name in [group.group_name for group in groups]):\n            if not('all' in [group.group_name for group in groups]):\n                return {'success': 'false', 'reason': 'Unauthorized Action'}\n        notify = Notification.query.filter_by(id=notify_id).first()\n        notify_info = {\n            'id': notify.id,\n            'title': notify.title,\n            'content': notify.content,\n            'create_date': notify.create_date\n        }\n        usernotifypair = UserNotificationPair.query.filter_by(userName=user.username, notifyId=notify.id).first()\n        usernotifypair.isRead = 1\n        db.session.add(usernotifypair)\n        db.session.commit()\n        if notify.status != 'open':\n            notify_info['title'] = 'This notification is not available'\n            notify_info['content'] = 'Sorry, it seems that the administrator has closed this notification.'\n            return {'success': 'false', 'data': notify_info}\n        return {'success': 'true', 'data': notify_info}\n"
  },
  {
    "path": "src/master/parser.py",
    "content": "#!/user/bin/python3\nimport json\n\njob_data = {'image_1': 'base_base_base', 'mappingRemoteDir_2_2': 'sss', 'dependency_1': 'aaa', 'mappingLocalDir_2_1': 'xxx', 'mappingLocalDir_1_2': 'aaa', 'mappingLocalDir_1_1': 'aaa', 'mappingLocalDir_2_3': 'fdsffdf', 'mappingRemoteDir_1_1': 'ddd', 'mappingRemoteDir_2_3': 'sss', 'srcAddr_1': 'aaa', 'mappingSource_2_1': 'Aliyun', 'cpuSetting_1': '1', 'mappingSource_2_2': 'Aliyun', 'retryCount_2': '1', 'mappingSource_1_1': 'Aliyun', 'expTime_1': '60', 'diskSetting_2': '1024', 'diskSetting_1': '1024', 'dependency_2': 'ddd', 'memorySetting_1': '1024', 'command_2': 'ccc', 'mappingRemoteDir_1_2': 'ddd', 'gpuSetting_2': '0', 'memorySetting_2': '1024', 'gpuSetting_1': '0', 'mappingLocalDir_2_2': 'bbb', 'mappingSource_1_2': 'Aliyun', 'expTime_2': '60', 'mappingRemoteDir_2_1': 'vvv', 'srcAddr_2': 'fff', 'cpuSetting_2': '1', 'instCount_1': '1', 'mappingSource_2_3': 'Aliyun', 'token': 'ZXlKaGJHY2lPaUpJVXpJMU5pSXNJbWxoZENJNk1UVXpNelE0TVRNMU5Td2laWGh3SWpveE5UTXpORGcwT1RVMWZRLmV5SnBaQ0k2TVgwLkF5UnRnaGJHZXhJY2lBSURZTUd5eXZIUVJnUGd1ZTA3OEtGWkVoejJVMkE=', 'instCount_2': '1', 'retryCount_1': '1', 'command_1': 'aaa', 'jobPriority': '0', 'image_2': 'base_base_base', 'jobName': 'aaa'}\n\ndef parse(job_data):\n    job_info = {}\n    message = {}\n    for key in job_data:\n        key_arr = key.split('_')\n        value = job_data[key]\n        if len(key_arr) == 1:\n            job_info[key_arr[0]] = value\n        elif len(key_arr) == 2:\n            key_prefix, task_idx = key_arr[0], key_arr[1]\n            task_idx = 'task_' + task_idx\n            if task_idx in job_info:\n                job_info[task_idx][key_prefix] = value\n            else:\n                tmp_dict = {\n                    key_prefix: value\n                }\n                job_info[task_idx] = tmp_dict\n        elif len(key_arr) == 3:\n            key_prefix, task_idx, mapping_idx = key_arr[0], key_arr[1], key_arr[2]\n            task_idx = 'task_' + task_idx\n            mapping_idx = 'mapping_' + mapping_idx\n            if task_idx in job_info:\n                if \"mapping\" in job_info[task_idx]:\n                    if mapping_idx in job_info[task_idx][\"mapping\"]:\n                        job_info[task_idx][\"mapping\"][mapping_idx][key_prefix] = value\n                    else:\n                        tmp_dict = {\n                            key_prefix: value\n                        }\n                        job_info[task_idx][\"mapping\"][mapping_idx] = tmp_dict\n                else:\n                    job_info[task_idx][\"mapping\"] = {\n                        mapping_idx: {\n                            key_prefix: value\n                        }\n                    }\n            else:\n                tmp_dict = {\n                    \"mapping\":{\n                        mapping_idx: {\n                            key_prefix: value\n                        }\n                    }\n                }\n                job_info[task_idx] = tmp_dict\n    print(json.dumps(job_info, indent=4))\n\nif __name__ == '__main__':\n    parse(job_data) \n"
  },
  {
    "path": "src/master/releasemgr.py",
    "content": "import threading, time, requests, json, traceback\nfrom utils import env\nfrom utils.log import logger\nfrom utils.model import db, VCluster, Container\nimport smtplib, datetime\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header\nfrom master.settings import settings\n\nuserpoint = \"http://\" + env.getenv('USER_IP') + \":\" + str(env.getenv('USER_PORT'))\ndef post_to_user(url = '/', data={}):\n    return requests.post(userpoint+url,data=data).json()\n\n_ONE_DAY_IN_SECONDS = 60 * 60 * 24\n\nclass ReleaseMgr(threading.Thread):\n\n    def __init__(self, vclustermgr, ulockmgr, check_interval=_ONE_DAY_IN_SECONDS):\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.vclustermgr = vclustermgr\n        self.ulockmgr = ulockmgr\n        self.check_interval = check_interval\n        self.warning_days = int(env.getenv(\"WARNING_DAYS\"))\n        self.release_days = int(env.getenv(\"RELEASE_DAYS\"))\n        if self.release_days <= self.warning_days:\n            self.release_days = self.warning_days+1\n        logger.info(\"[ReleaseMgr] start withe warning_days=%d release_days=%d\"%(self.warning_days, self.release_days))\n\n    def _send_email(self, to_address, username, vcluster, days, is_released=True):\n        email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n        if (email_from_address in ['\\'\\'', '\\\"\\\"', '']):\n            return\n        text = '<html><h4>Dear '+ username + ':</h4>'\n        st_str = vcluster.stop_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Your workspace/vcluster(name:%s id:%d) in <a href='%s'>%s</a>\n                   has been stopped more than %d days now(stopped at:%s). </p>\n                ''' % (vcluster.clustername, vcluster.clusterid, env.getenv(\"PORTAL_URL\"), env.getenv(\"PORTAL_URL\"), days, st_str)\n        if is_released:\n            text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Therefore, the workspace/vcluster has been released now.</p>\n                       <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>And the data in it couldn't be recoverd</b> unless you save it.</p>\n                       <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;You can create new workspace/vcluster if you need.</p>\n                    '''\n        else:\n            #day_d = self.release_days - (datetime.datetime.now() - vcluster.stop_time).days\n            release_date = vcluster.stop_time + datetime.timedelta(days=self.release_days)\n            day_d = (release_date - datetime.datetime.now()).days\n            rd_str = release_date.strftime(\"%Y-%m-%d %H:%M:%S\")\n            text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;It will be released after <b>%s(in about %d days)</b>.</p>\n                       <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<b>And the data in it couldn't be recoverd after releasing.</b></p>\n                       <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Please start or save it before <b>%s(in about %d days)</b> if you want to keep the data.</p>\n                    ''' % (rd_str, day_d, rd_str, day_d)\n        text += '''<br>\n                   <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Note: DO NOT reply to this email!</p>\n                   <br><br>\n                   <p> <a href='http://docklet.unias.org'>Docklet Team</a>, SEI, PKU</p>\n                '''\n        subject = 'Docklet workspace/vcluster releasing alert'\n        msg = MIMEMultipart()\n        textmsg = MIMEText(text,'html','utf-8')\n        msg['Subject'] = Header(subject, 'utf-8')\n        msg['From'] = email_from_address\n        msg['To'] = to_address\n        msg.attach(textmsg)\n        s = smtplib.SMTP()\n        s.connect()\n        try:\n            s.sendmail(email_from_address, to_address, msg.as_string())\n        except Exception as err:\n            logger.error(traceback.format_exc())\n        s.close()\n\n    def run(self):\n        while not self.thread_stop:\n            logger.info(\"[ReleaseMgr] Begin checking each vcluster if it needs to be released...\")\n\n            auth_key = env.getenv('AUTH_KEY')\n            res = post_to_user(\"/master/user/groupinfo/\", {'auth_key':auth_key})\n            groups = json.loads(res['groups'])\n            quotas = {}\n            for group in groups:\n                quotas[group['name']] = group['quotas']\n\n            vcs = VCluster.query.filter_by(status='stopped').all()\n            #logger.info(str(vcs))\n            for vc in vcs:\n                vc = VCluster.query.get(vc.clusterid)\n                if vc.stop_time is None:\n                    continue\n                days = (datetime.datetime.now() - vc.stop_time).days\n\n                if days >= self.release_days:\n                    logger.info(\"[ReleaseMgr] VCluster(id:%d,user:%s) has been stopped(%s) for more than %d days, it will be released.\"\n                                % (vc.clusterid, vc.ownername, vc.stop_time.strftime(\"%Y-%m-%d %H:%M:%S\"), self.release_days))\n                    rc_info = post_to_user(\"/master/user/recoverinfo/\", {'username':vc.ownername,'auth_key':auth_key})\n                    logger.info(\"[ReleaseMgr] %s\"%str(rc_info))\n                    groupname = rc_info['groupname']\n                    user_info = {\"data\":{\"id\":rc_info['uid'],\"group\":groupname,\"groupinfo\":quotas[groupname]}}\n                    self.ulockmgr.acquire(vc.ownername)\n                    try:\n                        [status, usage_info] = self.vclustermgr.get_clustersetting(vc.clustername, vc.ownername, \"all\", True)\n                        success, msg = self.vclustermgr.delete_cluster(vc.clustername, vc.ownername, json.dumps(user_info))\n                        if not success:\n                            logger.error(\"[ReleaseMgr] Can't release VCluster(id:%d,user:%s) for %s\"%(vc.clusterid, vc.ownername, msg))\n                        else:\n                            if status:\n                                logger.info(\"[ReleaseMgr] Release Quota.\")\n                                post_to_user(\"/master/user/usageRelease/\", {'auth_key':auth_key,'username':vc.ownername,\n                                             'cpu':usage_info['cpu'], 'memory':usage_info['memory'],'disk':usage_info['disk']})\n                            self._send_email(rc_info['email'], vc.ownername, vc, self.release_days)\n                            logger.info(\"[ReleaseMgr] Succeed to releasing VCluster(id:%d,user:%s) for %s. Send mail to info.\"%(vc.clusterid, vc.ownername, msg))\n                    except Exception as err:\n                        logger.error(traceback.format_exc())\n                    finally:\n                        self.ulockmgr.release(vc.ownername)\n\n                elif days >= self.warning_days:\n                    logger.info(\"[ReleaseMgr] VCluster(id:%d,user:%s) has been stopped(%s) for more than %d days. A warning email will be sent to the user.\"\n                                % (vc.clusterid, vc.ownername, vc.stop_time.strftime(\"%Y-%m-%d %H:%M:%S\"), self.warning_days))\n                    if vc.is_warned:\n                        logger.info(\"[ReleaseMgr] VCluster(id:%d,user:%s) has been warned before. Skip it.\"% (vc.clusterid, vc.ownername))\n                        continue\n                    rc_info = post_to_user(\"/master/user/recoverinfo/\", {'username':vc.ownername,'auth_key':auth_key})\n                    logger.info(\"[ReleaseMgr] %s\"%str(rc_info))\n                    self._send_email(rc_info['email'], vc.ownername, vc, self.warning_days, False)\n                    vc.is_warned = True\n\n                    try:\n                        db.session.commit()\n                    except Exception as err:\n                        db.session.rollback()\n                        logger.warning(traceback.format_exc())\n            time.sleep(self.check_interval)\n\n    def stop(self):\n        self.thread_stop = True\n        return\n"
  },
  {
    "path": "src/master/settings.py",
    "content": "#!/usr/bin/python3\n\nfrom utils import env\nimport json, os\nfrom functools import wraps\nfrom utils.log import logger\n\n\nclass settingsClass:\n    setting = {}\n    def __init__(self):\n        settingPath = env.getenv('FS_PREFIX') + '/local/settings.conf'\n        if not os.path.exists(settingPath):\n            settingFile = open(settingPath,'w')\n            setting = {}\n            settingFile.write(json.dumps(setting))\n            settingFile.close()\n        else:\n            settingFile = open(settingPath, 'r')\n            settingText = settingFile.read()\n            settingFile.close()\n            self.setting = json.loads(settingText)\n\n    def get(self, arg):\n        return self.setting.get(arg,'')\n\n    def list(*args, **kwargs):\n        if ( ('user_group' in kwargs) == False):\n            return {\"success\":'false', \"reason\":\"Cannot get user_group\"}\n        user_group = kwargs['user_group']\n        if (not ((user_group == 'admin') or (user_group == 'root'))):\n            return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n        return {'success': 'true', 'result': args[0].setting}\n\n    def update(*args, **kwargs):\n        try:\n            if ( ('user_group' in kwargs) == False):\n                return {\"success\":'false', \"reason\":\"Cannot get user_group\"}\n            user_group = kwargs['user_group']\n            if (not ((user_group == 'admin') or (user_group == 'root'))):\n                return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n            newSetting = kwargs['newSetting']\n            settingPath = env.getenv('FS_PREFIX') + '/local/settings.conf';\n            settingText = json.dumps(newSetting)\n            settingFile = open(settingPath,'w')\n            settingFile.write(settingText)\n            settingFile.close()\n            args[0].setting = newSetting\n            return {'success': 'true'}\n        except:\n            return {'success': 'false'}\n\n\nsettings = settingsClass()\n"
  },
  {
    "path": "src/master/sysmgr.py",
    "content": "import re, string, os\r\n\r\n\r\neditableParms = [\"LOG_LEVEL\",\"ADMIN_EMAIL_ADDRESS\",\"EMAIL_FROM_ADDRESS\",\"OPEN_REGISTRY\",\"APPROVAL_RBT\"]\r\nconfigPath = {\"docklet\": os.environ.get(\"DOCKLET_CONF\")+\"/docklet.conf\",\r\n    \"container\": os.environ.get(\"DOCKLET_CONF\")+\"/container.conf\"}\r\n#configPath = {\"docklet\": \"../conf/docklet.conf\",\r\n#    \"container\": \"../conf/container.conf\"}\r\ndefaultPattern = re.compile(u'# *\\S+ *= *\\S+')\r\nactivePattern = re.compile(u'\\S+ *= *\\S+')\r\nhistoryPattern = re.compile(u'## *\\S+ *= *\\S+')\r\n\r\ndef parse_line(line):\r\n    kind = \"\"\r\n    parm = \"\"\r\n    val = \"\"\r\n    if defaultPattern.match(line) != None and not \"==\" in line:\r\n        kind = \"default\"\r\n    elif activePattern.match(line) != None and not \"#\" in line:\r\n        kind = \"active\"\r\n    elif historyPattern.match(line) != None and not \"==\" in line:\r\n        kind = \"history\"\r\n    if kind != \"\":\r\n        line = line.replace(\"#\", \"\").replace(\"\\n\", \"\")\r\n        parm = line[:line.find(\"=\")].strip()\r\n        val = line[line.find(\"=\")+1:].strip()\r\n    return [kind, parm, val]\r\n\r\nclass SystemManager():\r\n\r\n    def getParmList(*args, **kwargs):\r\n        #result = {\"docklet\": \"\", \"container\": \"\"}\r\n        result = {\"docklet\": \"\", \"container\": \"\"}\r\n        for field in [\"docklet\"]:\r\n            configFile = open(configPath[field])\r\n            lines = configFile.readlines()\r\n            configFile.close()\r\n            configFile = open(configPath[field])\r\n            wholeFile = configFile.read()\r\n            configFile.close()\r\n            conf = {}\r\n            segs = wholeFile.split(\"\\n\\n\")\r\n            for line in lines:\r\n                [linekind, lineparm, lineval] = parse_line(line)\r\n                if lineparm in editableParms:\r\n                    editable = 1 # edit it in settings.py\r\n                else:\r\n                    editable = 0\r\n                if linekind == \"default\":\r\n                    conf[lineparm] = {\"val\": \"novalidvaluea\", \"default\": lineval,\r\n                        \"history\": [], \"editable\": editable, \"details\": \"\"}\r\n            for line in lines:\r\n                [linekind, lineparm, lineval] = parse_line(line)\r\n                if linekind == \"active\":\r\n                    try:\r\n                        conf[lineparm][\"val\"] = lineval\r\n                    except:\r\n                        if lineparm in editableParms:\r\n                            editable = 1\r\n                        else:\r\n                            editable = 0\r\n                        conf[lineparm] = {\"val\": lineval, \"default\": lineval,\r\n                            \"history\": [], \"editable\": editable, \"details\": \"\"}\r\n            for line in lines:\r\n                [linekind, lineparm, lineval] = parse_line(line)\r\n                if linekind == \"history\":\r\n                    conf[lineparm][\"history\"].append(lineval)\r\n            for parm in conf.keys():\r\n                for seg in segs:\r\n                    if parm in seg:\r\n                        conf[parm][\"details\"] = seg\r\n            result[field] = [({'parm': parm, 'val': conf[parm]['val'],\r\n                'default': conf[parm]['default'], \"history\": conf[parm]['history'],\r\n                \"editable\": conf[parm]['editable'], \"details\": conf[parm]['details']}) for parm in sorted(conf.keys())]\r\n        configFile = open(configPath[\"container\"])\r\n        wholeFile = configFile.read()\r\n        configFile.close()\r\n        result[\"container\"] = wholeFile\r\n        return result\r\n\r\n    # 1. def and not act 2. act and not def 3. def and act\r\n    # have def and act and hist\r\n    def modify(self, field, parm, val):\r\n        configFile = open(configPath[field])\r\n        lines = configFile.readlines()\r\n        configFile.close()\r\n        finish = False\r\n        for i in range(0, len(lines)):\r\n            line = lines[i]\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"active\" and lineparm == parm:\r\n                lines[i] = \"## \" + parm + \"=\" + lineval + \"\\n\"\r\n                lines.insert(i, parm + \"=\" + val + \"\\n\")\r\n                if i == 0 or not parm in lines[i-1] or not \"=\" in lines[i-1]:\r\n                    lines.insert(i, \"# \" + parm + \"=\" + lineval + \"\\n\")\r\n                finish = True\r\n                break\r\n        if finish == False:\r\n            for i in range(0, len(lines)):\r\n                line = lines[i]\r\n                [linekind, lineparm, lineval] = parse_line(line)\r\n                if linekind == \"default\" and parm == lineparm:\r\n                    lines.insert(i+1, parm + \"=\"  + val + \"\\n\")\r\n                    break\r\n        for i in range(0, len(lines)):\r\n            line = lines[i]\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"history\" and parm == lineparm and val == lineval:\r\n                lines.pop(i)\r\n                break\r\n        configFile = open(configPath[field], \"w\")\r\n        for line in lines:\r\n            configFile.write(line)\r\n        configFile.close()\r\n        os.environ[parm] = val\r\n        return [True, \"\"]\r\n\r\n    def clear(self, field, parm):\r\n        configFile = open(configPath[field])\r\n        lines = configFile.readlines()\r\n        configFile.close()\r\n        finish = False\r\n        for i in range(0, len(lines)):\r\n            line = lines[i]\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"history\" and parm == lineparm:\r\n                lines[i] = \"\"\r\n        configFile = open(configPath[field], \"w\")\r\n        for line in lines:\r\n            configFile.write(line)\r\n        configFile.close()\r\n        return [True, \"\"]\r\n\r\n    def add(self, field, parm, val):\r\n        configFile = open(configPath[field], \"a\")\r\n        configFile.write(\"\\n\" + \"# \" + parm + \"=\" + val + \"\\n\" + parm + \"=\" + val + \"\\n\")\r\n        configFile.close()\r\n        return [True, \"\"]\r\n\r\n    def delete(self, field, parm):\r\n        configFile = open(configPath[field])\r\n        lines = configFile.readlines()\r\n        configFile.close()\r\n        for i in range(0, len(lines)):\r\n            line = lines[i]\r\n            if parm in line:\r\n                lines[i] = \"\"\r\n        configFile = open(configPath[field], \"w\")\r\n        for line in lines:\r\n            configFile.write(line)\r\n        configFile.close()\r\n        return [True, \"\"]\r\n\r\n    def reset_all(self, field):\r\n        configFile = open(configPath[field])\r\n        lines = configFile.readlines()\r\n        configFile.close()\r\n        conf = {}\r\n        for line in lines:\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"default\":\r\n                conf[lineparm] = {\"val\": lineval, \"default\": lineval, \"history\": []}\r\n        for line in lines:\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"active\":\r\n                try:\r\n                    conf[lineparm][\"val\"] = lineval\r\n                except:\r\n                    conf[lineparm] = {\"val\": lineval, \"default\": lineval, \"history\": []}\r\n        for line in lines:\r\n            [linekind, lineparm, lineval] = parse_line(line)\r\n            if linekind == \"history\":\r\n                conf[lineparm][\"history\"].append(lineval)\r\n\r\n        for i in range(0, len(lines)):\r\n            line = lines[i]\r\n            if activePattern.match(line) != None and not \"#\" in line:\r\n                segs = line.replace(\"\\n\", \"\").split(\"=\")\r\n                lines[i] = segs[0].strip() + \"=\" + conf[segs[0].strip()][\"default\"] + \"\\n\"\r\n            elif historyPattern.match(line) != None and not \"==\" in line:\r\n                lines[i] = \"\"\r\n        configFile = open(configPath[field], \"w\")\r\n        for line in lines:\r\n            configFile.write(line)\r\n        configFile.close()\r\n        return [True, \"\"]\r\n\r\n#sysmgr = SystemManager()\r\n#print(sysmgr.getParmList())\r\n"
  },
  {
    "path": "src/master/taskmgr.py",
    "content": "import threading\nimport time\nimport string\nimport os\nimport random, copy, subprocess\nimport json, math\nfrom functools import wraps\n\n# must import logger after initlogging, ugly\nfrom utils.log import logger\n\n# grpc\nfrom concurrent import futures\nimport grpc\nfrom protos.rpc_pb2 import *\nfrom protos.rpc_pb2_grpc import MasterServicer, add_MasterServicer_to_server, WorkerStub\nfrom utils.nettools import netcontrol\nfrom utils import env\n\ndef ip_to_int(addr):\n    [a, b, c, d] = addr.split('.')\n    return (int(a)<<24) + (int(b)<<16) + (int(c)<<8) + int(d)\n\ndef int_to_ip(num):\n    return str((num>>24)&255)+\".\"+str((num>>16)&255)+\".\"+str((num>>8)&255)+\".\"+str(num&255)\n\nclass Task():\n    def __init__(self, taskmgr, task_id, username, at_same_time, priority, max_size, task_infos):\n        self.taskmgr = taskmgr\n        self.id = task_id\n        self.username = username\n        self.status = WAITING\n        self.failed_reason = \"\"\n        # if all the vnodes must be started at the same time\n        self.at_same_time = at_same_time\n        # priority the bigger the better\n        # self.priority the smaller the better\n        self.priority = int(time.time()) / 60 / 60 - priority\n        self.task_base_ip = None\n        self.ips = None\n        self.max_size = max_size\n        self.gpu_preference = task_infos[0]['gpu_preference']\n        self.order = -1 # scheduling order of the task\n\n        self.subtask_list = [SubTask(\n                idx = index,\n                root_task = self,\n                vnode_info = task_info['vnode_info'],\n                command_info = task_info['command_info'],\n                max_retry_count = task_info['max_retry_count'],\n                gpu_preference = task_info['gpu_preference']\n            ) for (index, task_info) in enumerate(task_infos)]\n\n    def get_billing(self):\n        billing_beans = 0\n        running_time = 0\n        cpu_price = 1 / 3600.0  # /core*s\n        mem_price = 1 / 3600.0 # /GB*s\n        disk_price = 1 / 3600.0 # /GB*s\n        gpu_price = 100 / 3600.0 # /core*s\n        for subtask in self.subtask_list:\n            tmp_time = subtask.running_time\n            cpu_beans = subtask.vnode_info.vnode.instance.cpu * tmp_time * cpu_price\n            mem_beans = subtask.vnode_info.vnode.instance.memory / 1024.0 * tmp_time * mem_price\n            disk_beans = subtask.vnode_info.vnode.instance.disk / 1024.0 * tmp_time * disk_price\n\n            worker_info = self.taskmgr.get_worker_resource_info(subtask.worker)\n            worker_gpu_price = worker_info['gpu_price'] / 3600.0\n            gpu_beans = subtask.vnode_info.vnode.instance.gpu * tmp_time * gpu_price\n\n            logger.info(\"subtask:%s running_time=%f beans for: cpu=%f mem_beans=%f disk_beans=%f gpu_beans=%f\"\n                        %(self.id, tmp_time, cpu_beans, mem_beans, disk_beans, gpu_beans ))\n            beans = math.ceil(cpu_beans + mem_beans + disk_beans + gpu_beans)\n            running_time += tmp_time\n            billing_beans += beans\n        return running_time, billing_beans\n\n    def __lt__(self, other):\n        return self.priority < other.priority\n\n    def gen_ips_from_base(self,base_ip):\n        if self.task_base_ip == None:\n            return\n        self.ips = []\n        for i in range(self.max_size):\n            self.ips.append(int_to_ip(base_ip + self.task_base_ip + i + 2))\n\n    def gen_hosts(self):\n        username = self.username\n        taskid = self.id\n        logger.info(\"Generate hosts for user(%s) task(%s) base_ip(%s)\"%(username,taskid,str(self.task_base_ip)))\n        fspath = env.getenv('FS_PREFIX')\n        if not os.path.isdir(\"%s/global/users/%s\" % (fspath,username)):\n            path = env.getenv('DOCKLET_LIB')\n            subprocess.call([path+\"/master/userinit.sh\", username])\n            logger.info(\"user %s directory not found, create it\" % username)\n\n        hosts_file = open(\"%s/global/users/%s/hosts/%s.hosts\" % (fspath,username,\"batch-\"+taskid),\"w\")\n        hosts_file.write(\"127.0.0.1 localhost\\n\")\n        i = 0\n        for ip in self.ips:\n            hosts_file.write(ip+\" batch-\"+str(i)+\"\\n\")\n            i += 1\n        hosts_file.close()\n\nclass SubTask():\n    def __init__(self, idx, root_task, vnode_info, command_info, max_retry_count, gpu_preference):\n        self.root_task = root_task\n        self.vnode_info = vnode_info\n        self.vnode_info.vnodeid = idx\n        self.command_info = command_info\n        if self.command_info != None:\n            self.command_info.vnodeid = idx\n        self.max_retry_count = max_retry_count\n        self.gpu_preference = gpu_preference\n        self.vnode_started = False\n        self.task_started = False\n        self.start_at = 0\n        self.end_at = 0\n        self.running_time = 0\n        self.status = WAITING\n        self.status_reason = ''\n        self.try_count = 0\n        self.worker = None\n        self.lock = threading.Lock()\n\n    def waiting_for_retry(self,reason=\"\"):\n        self.try_count += 1\n        self.status = WAITING if self.try_count <= self.max_retry_count else FAILED\n        if self.status == FAILED:\n            self.root_task.status = FAILED\n            self.failed_reason = reason\n            self.root_task.failed_reason = reason\n\nclass TaskReporter(MasterServicer):\n\n    def __init__(self, taskmgr):\n        self.taskmgr = taskmgr\n\n    def report(self, request, context):\n        for task_report in request.taskmsgs:\n            self.taskmgr.on_task_report(task_report)\n        return Reply(status=Reply.ACCEPTED, message='')\n\n\nclass TaskMgr(threading.Thread):\n\n    # load task information from etcd\n    # initial a task queue and task schedueler\n    # taskmgr: a taskmgr instance\n    def __init__(self, nodemgr, monitor_fetcher, master_ip, scheduler_interval=2, external_logger=None):\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.jobmgr = None\n        self.master_ip = master_ip\n        self.task_queue = []\n        self.lazy_append_list = []\n        self.lazy_delete_list = []\n        self.lazy_stop_list = []\n        self.task_queue_lock = threading.Lock()\n        self.stop_lock = threading.Lock()\n        self.add_lock = threading.Lock()\n        #self.user_containers = {}\n\n        self.scheduler_interval = scheduler_interval\n        self.logger = logger\n\n        self.master_port = env.getenv('BATCH_MASTER_PORT')\n        self.worker_port = env.getenv('BATCH_WORKER_PORT')\n\n        # nodes\n        self.nodemgr = nodemgr\n        self.monitor_fetcher = monitor_fetcher\n        self.cpu_usage = {}\n        self.gpu_usage = {}\n        # self.all_nodes = None\n        # self.last_nodes_info_update_time = 0\n        # self.nodes_info_update_interval = 30 # (s)\n\n        self.gpu_pending_tasks = {}\n\n        self.network_lock = threading.Lock()\n        batch_net = env.getenv('BATCH_NET')\n        self.batch_cidr = int(batch_net.split('/')[1])\n        batch_net = batch_net.split('/')[0]\n        task_cidr = int(env.getenv('BATCH_TASK_CIDR'))\n        task_cidr = min(task_cidr,31-self.batch_cidr)\n        self.task_cidr = max(task_cidr,2)\n        self.base_ip = ip_to_int(batch_net)\n        self.free_nets = []\n        for i in range(0, (1 << (32-self.batch_cidr)) - 1, (1 << self.task_cidr)):\n            self.free_nets.append(i)\n        #self.logger.info(\"Free nets addresses pool %s\" % str(self.free_nets))\n        self.logger.info(\"Each Batch Net CIDR:%s\"%(str(self.task_cidr)))\n\n    def data_lock(lockname):\n        def lock(f):\n            @wraps(f)\n            def new_f(self, *args, **kwargs):\n                lockobj = getattr(self,lockname)\n                lockobj.acquire()\n                try:\n                    result = f(self, *args, **kwargs)\n                except Exception as err:\n                    lockobj.release()\n                    raise err\n                lockobj.release()\n                return result\n            return new_f\n        return lock\n\n    def subtask_lock(f):\n        @wraps(f)\n        def new_f(self, subtask, *args, **kwargs):\n            subtask.lock.acquire()\n            try:\n                result = f(self, subtask, *args, **kwargs)\n            except Exception as err:\n                subtask.lock.release()\n                raise err\n            subtask.lock.release()\n            return result\n        return new_f\n\n    def run(self):\n        self.serve()\n        while not self.thread_stop:\n            self.sort_out_task_queue()\n            task, sub_task_list = self.task_scheduler()\n            if task is not None and sub_task_list is not None:\n                self.task_processor(task, sub_task_list)\n            else:\n                time.sleep(self.scheduler_interval)\n\n    def serve(self):\n        self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))\n        add_MasterServicer_to_server(TaskReporter(self), self.server)\n        self.server.add_insecure_port('[::]:' + self.master_port)\n        self.server.start()\n        self.logger.info('[taskmgr_rpc] start rpc server')\n\n    def stop(self):\n        self.thread_stop = True\n        self.server.stop(0)\n        self.logger.info('[taskmgr_rpc] stop rpc server')\n\n    @data_lock('task_queue_lock')\n    @data_lock('add_lock')\n    @data_lock('stop_lock')\n    def sort_out_task_queue(self):\n\n        for task in self.task_queue:\n            if task.id in self.lazy_stop_list:\n                self.stop_remove_task(task)\n                self.lazy_delete_list.append(task)\n                running_time, billing = task.get_billing()\n                self.logger.info('task %s stopped, running_time:%s billing:%d'%(task.id, str(running_time), billing))\n                running_time = math.ceil(running_time)\n                self.jobmgr.report(task.username, task.id,'stopped',running_time=running_time,billing=billing)\n\n        while self.lazy_delete_list:\n            task = self.lazy_delete_list.pop(0)\n            try:\n                self.task_queue.remove(task)\n            except Exception as err:\n                self.logger.warning(str(err))\n\n        new_append_list = []\n        for task in self.lazy_append_list:\n            if task.id in self.lazy_stop_list:\n                self.jobmgr.report(task.username, task.id, 'stopped')\n            else:\n                new_append_list.append(task)\n\n        self.lazy_append_list = new_append_list\n        self.lazy_stop_list.clear()\n        if self.lazy_append_list:\n            self.task_queue.extend(self.lazy_append_list)\n            self.lazy_append_list.clear()\n            self.task_queue = sorted(self.task_queue, key=lambda x: x.priority)\n\n        self.gpu_pending_tasks = {}\n        no_pref_task_counts = 0\n        for task in self.task_queue:\n            if task.gpu_preference == 'null':\n                task.order = no_pref_task_counts\n                no_pref_task_counts += 1\n            else:\n                if task.gpu_preference not in self.gpu_pending_tasks:\n                    self.gpu_pending_tasks[task.gpu_preference] = 0\n                task.order = no_pref_task_counts + self.gpu_pending_tasks[task.gpu_preference]\n                self.gpu_pending_tasks[task.gpu_preference] += 1\n        self.gpu_pending_tasks['null'] = no_pref_task_counts\n\n    def start_vnode(self, subtask):\n        try:\n            self.logger.info('[task_processor] Starting vnode for task [%s] vnode [%d]' % (subtask.vnode_info.taskid, subtask.vnode_info.vnodeid))\n            channel = grpc.insecure_channel('%s:%s' % (subtask.worker, self.worker_port))\n            stub = WorkerStub(channel)\n            response = stub.start_vnode(subtask.vnode_info)\n            if response.status != Reply.ACCEPTED:\n                raise Exception(response.message)\n        except Exception as e:\n            self.logger.error('[task_processor] rpc error message: %s' % e)\n            subtask.status_reason = str(e)\n            return [False, e]\n        subtask.vnode_started = True\n        subtask.start_at = time.time()\n        self.cpu_usage[subtask.worker] += subtask.vnode_info.vnode.instance.cpu\n        self.gpu_usage[subtask.worker] += subtask.vnode_info.vnode.instance.gpu\n        return [True, '']\n\n    @subtask_lock\n    def stop_vnode(self, subtask):\n        if not subtask.vnode_started:\n            return [True, \"\"]\n        try:\n            self.logger.info('[task_processor] Stopping vnode for task [%s] vnode [%d]' % (subtask.vnode_info.taskid, subtask.vnode_info.vnodeid))\n            channel = grpc.insecure_channel('%s:%s' % (subtask.worker, self.worker_port))\n            stub = WorkerStub(channel)\n            response = stub.stop_vnode(subtask.vnode_info)\n            if response.status != Reply.ACCEPTED:\n                raise Exception(response.message)\n        except Exception as e:\n            self.logger.error('[task_processor] rpc error message: %s' % e)\n            subtask.status_reason = str(e)\n            return [False, e]\n        subtask.vnode_started = False\n        subtask.end_at = time.time()\n        subtask.running_time += subtask.end_at - subtask.start_at\n        self.cpu_usage[subtask.worker] -= subtask.vnode_info.vnode.instance.cpu\n        self.gpu_usage[subtask.worker] -= subtask.vnode_info.vnode.instance.gpu\n        return [True, '']\n\n    def start_subtask(self, subtask):\n        try:\n            self.logger.info('[task_processor] Starting task [%s] vnode [%d]' % (subtask.vnode_info.taskid, subtask.vnode_info.vnodeid))\n            channel = grpc.insecure_channel('%s:%s' % (subtask.worker, self.worker_port))\n            stub = WorkerStub(channel)\n            response = stub.start_task(subtask.command_info)\n            if response.status != Reply.ACCEPTED:\n                raise Exception(response.message)\n        except Exception as e:\n            self.logger.error('[task_processor] rpc error message: %s' % e)\n            subtask.status_reason = str(e)\n            return [False, e]\n        subtask.task_started = True\n        return [True, '']\n\n    def stop_subtask(self, subtask):\n        try:\n            self.logger.info('[task_processor] Stopping task [%s] vnode [%d]' % (subtask.vnode_info.taskid, subtask.vnode_info.vnodeid))\n            channel = grpc.insecure_channel('%s:%s' % (subtask.worker, self.worker_port))\n            stub = WorkerStub(channel)\n            response = stub.stop_task(subtask.command_info)\n            if response.status != Reply.ACCEPTED:\n                raise Exception(response.message)\n        except Exception as e:\n            self.logger.error('[task_processor] rpc error message: %s' % e)\n            subtask.status = FAILED\n            subtask.status_reason = str(e)\n            return [False, e]\n        subtask.task_started = False\n        return [True, '']\n\n    @data_lock('network_lock')\n    def acquire_task_ips(self, task):\n        self.logger.info(\"[acquire_task_ips] user(%s) task(%s) net(%s)\" % (task.username, task.id, str(task.task_base_ip)))\n        if task.task_base_ip == None:\n            task.task_base_ip = self.free_nets.pop(0)\n        return task.task_base_ip\n\n    @data_lock('network_lock')\n    def release_task_ips(self, task):\n        self.logger.info(\"[release_task_ips] user(%s) task(%s) net(%s)\" % (task.username, task.id, str(task.task_base_ip)))\n        if task.task_base_ip == None:\n            return\n        self.free_nets.append(task.task_base_ip)\n        task.task_base_ip = None\n        #self.logger.error('[release task_net] %s' % str(e))\n\n    def setup_tasknet(self, task, workers=None):\n        taskid = task.id\n        username = task.username\n        brname = \"docklet-batch-%s-%s\"%(username, taskid)\n        gwname = taskid\n        if task.task_base_ip == None:\n            return [False, \"task.task_base_ip is None!\"]\n        gatewayip = int_to_ip(self.base_ip + task.task_base_ip + 1)\n        gatewayipcidr = gatewayip + \"/\" + str(32-self.task_cidr)\n        netcontrol.new_bridge(brname)\n        netcontrol.setup_gw(brname,gwname,gatewayipcidr,0,0)\n\n        for wip in workers:\n            if wip != self.master_ip:\n                netcontrol.setup_gre(brname,wip)\n        return [True, gatewayip]\n\n    def remove_tasknet(self, task):\n        taskid = task.id\n        username = task.username\n        brname = \"docklet-batch-%s-%s\"%(username, taskid)\n        netcontrol.del_bridge(brname)\n\n    def task_processor(self, task, sub_task_list):\n        task.status = RUNNING\n        self.jobmgr.report(task.username, task.id, 'running')\n\n        # properties for transactio\n\n        self.acquire_task_ips(task)\n        task.gen_ips_from_base(self.base_ip)\n        task.gen_hosts()\n        #need to create hosts\n        [success, gwip] = self.setup_tasknet(task, [sub_task.worker for sub_task in sub_task_list])\n        if not success:\n            self.release_task_ips(task)\n            return [False, gwip]\n\n        placed_workers = []\n\n        start_all_vnode_success = True\n        # start vc\n        for sub_task in sub_task_list:\n            vnode_info = sub_task.vnode_info\n            vnode_info.vnode.hostname = \"batch-\" + str(vnode_info.vnodeid % task.max_size)\n            if sub_task.vnode_started:\n                continue\n\n            username = sub_task.root_task.username\n            #container_name = task.info.username + '-batch-' + task.info.id + '-' + str(instance_id) + '-' + task.info.token\n            #if not username in self.user_containers.keys():\n                #self.user_containers[username] = []\n            #self.user_containers[username].append(container_name)\n            ipaddr = task.ips[vnode_info.vnodeid % task.max_size] + \"/\" + str(32-self.task_cidr)\n            brname = \"docklet-batch-%s-%s\" % (username, sub_task.root_task.id)\n            networkinfo = Network(ipaddr=ipaddr, gateway=gwip, masterip=self.master_ip, brname=brname)\n            vnode_info.vnode.network.CopyFrom(networkinfo)\n\n            placed_workers.append(sub_task.worker)\n            [success, msg] = self.start_vnode(sub_task)\n            if not success:\n                sub_task.waiting_for_retry(\"Fail to start vnode.\")\n                if sub_task.status == WAITING:\n                    self.jobmgr.report(task.username, task.id, 'retrying', \"Fail to start vnode.\")\n                sub_task.worker = None\n                start_all_vnode_success = False\n\n        if not start_all_vnode_success:\n            return\n\n        # start tasks\n        for sub_task in sub_task_list:\n            task_info = sub_task.command_info\n            if task_info is None or sub_task.status == RUNNING:\n                sub_task.status = RUNNING\n                continue\n            task_info.token = ''.join(random.sample(string.ascii_letters + string.digits, 8))\n\n            [success, msg] = self.start_subtask(sub_task)\n            if success:\n                sub_task.status = RUNNING\n            else:\n                sub_task.waiting_for_retry(\"Fail to start task.\")\n                if sub_task.status == WAITING:\n                    self.jobmgr.report(task.username, task.id, 'retrying', \"Fail to start task.\")\n\n    def clear_sub_tasks(self, sub_task_list):\n        for sub_task in sub_task_list:\n            self.clear_sub_task(sub_task)\n\n    def clear_sub_task(self, sub_task):\n        if sub_task.task_started:\n            self.stop_subtask(sub_task)\n            #pass\n        if sub_task.vnode_started:\n            self.stop_vnode(sub_task)\n            #pass\n\n    @data_lock('stop_lock')\n    def lazy_stop_task(self, taskid):\n        self.lazy_stop_list.append(taskid)\n\n    def stop_remove_task(self, task):\n        if task is None:\n            return\n        self.logger.info(\"[taskmgr] stop and remove task(%s)\"%task.id)\n        self.clear_sub_tasks(task.subtask_list)\n        self.release_task_ips(task)\n        self.remove_tasknet(task)\n\n    def check_task_completed(self, task):\n        if task.status == RUNNING or task.status == WAITING:\n            for sub_task in task.subtask_list:\n                if sub_task.command_info != None and (sub_task.status == RUNNING or sub_task.status == WAITING):\n                    return False\n        self.logger.info('task %s finished, status %d, subtasks: %s' % (task.id, task.status, str([sub_task.status for sub_task in task.subtask_list])))\n        self.stop_remove_task(task)\n        self.lazy_delete_list.append(task)\n        running_time, billing = task.get_billing()\n        self.logger.info('task %s running_time:%s billing:%d'%(task.id, str(running_time), billing))\n        running_time = math.ceil(running_time)\n        if task.status == FAILED:\n            self.jobmgr.report(task.username,task.id,\"failed\",task.failed_reason,task.subtask_list[0].max_retry_count+1, running_time, billing)\n        else:\n            self.jobmgr.report(task.username,task.id,'finished',running_time=running_time,billing=billing)\n        return True\n\n    # this method is called when worker send heart-beat rpc request\n    def on_task_report(self, report):\n        self.logger.info('[on_task_report] receive task report: id %s-%d, status %d' % (report.taskid, report.vnodeid, report.subTaskStatus))\n        task = self.get_task(report.taskid)\n        if task == None:\n            self.logger.error('[on_task_report] task not found')\n            return\n\n        sub_task = task.subtask_list[report.vnodeid]\n        if sub_task.command_info.token != report.token:\n            self.logger.warning('[on_task_report] wrong token, %s %s' % (sub_task.command_info.token, report.token))\n            return\n        username = task.username\n        # container_name = username + '-batch-' + task.info.id + '-' + str(report.instanceid) + '-' + report.token\n        # self.user_containers[username].remove(container_name)\n\n        if sub_task.status != RUNNING:\n            self.logger.error('[on_task_report] receive task report when vnode is not running')\n\n        #sub_task.status = report.subTaskStatus\n        sub_task.status_reason = report.errmsg\n        sub_task.task_started = False\n\n        if report.subTaskStatus == FAILED or report.subTaskStatus == TIMEOUT:\n            self.clear_sub_task(sub_task)\n            sub_task.waiting_for_retry(report.errmsg)\n            self.logger.info('task %s report failed, status %d, subtasks: %s' % (task.id, task.status, str([sub_task.status for sub_task in task.subtask_list])))\n            if sub_task.status == WAITING:\n                self.jobmgr.report(task.username, task.id, 'retrying', report.errmsg)\n        elif report.subTaskStatus == OUTPUTERROR:\n            self.clear_sub_task(sub_task)\n            sub_task.status = FAILED\n            task.status = FAILED\n            task.failed_reason = report.errmsg\n        elif report.subTaskStatus == COMPLETED:\n            sub_task.status = report.subTaskStatus\n            self.clear_sub_task(sub_task)\n\n    # return task, workers\n    def task_scheduler(self):\n        # simple FIFO with priority\n        self.logger.info('[task_scheduler] scheduling... (%d tasks remains)' % len(self.task_queue))\n\n        gpu_has_pending_task = set()\n\n        for task in self.task_queue:\n            if task in self.lazy_delete_list or task.id in self.lazy_stop_list:\n                continue\n            self.logger.info('task %s sub_tasks %s' % (task.id, str([sub_task.status for sub_task in task.subtask_list])))\n            if self.check_task_completed(task):\n                continue\n            self.logger.info('schedule task %s sub_tasks %s' % (task.id, str([sub_task.status for sub_task in task.subtask_list])))\n\n            if task.at_same_time:\n                # parallel tasks\n                if not self.has_waiting(task.subtask_list):\n                    continue\n\n                # 如果偏好的gpu类型前面已经有其他任务在等待了，就直接跳过调度\n                if task.gpu_preference in gpu_has_pending_task:\n                    continue\n\n                workers = self.find_proper_workers(task.subtask_list)\n                if len(workers) == 0:\n                    # 如果找不到合适的节点，且存在gpu偏好，则允许不需要该gpu的节点先行调度\n                    if task.gpu_preference is not None and task.gpu_preference != 'null':\n                        gpu_has_pending_task.add(task.gpu_preference)\n                        continue\n                    return None, None\n                else:\n                    for i in range(len(workers)):\n                        task.subtask_list[i].worker = workers[i]\n                    return task, task.subtask_list\n            else:\n                # traditional tasks\n                has_waiting = False\n                for sub_task in task.subtask_list:\n                    if sub_task.status == WAITING:\n                        has_waiting = True\n                        # 如果偏好的gpu类型前面已经有其他任务在等待了，就直接跳过调度\n                        if sub_task.gpu_preference in gpu_has_pending_task:\n                            continue\n                        workers = self.find_proper_workers([sub_task])\n                        if len(workers) > 0:\n                            sub_task.worker = workers[0]\n                            return task, [sub_task]\n                if has_waiting:\n                    # 如果找不到合适的节点，且存在gpu偏好，则允许不需要该gpu的节点先行调度\n                    if task.gpu_preference is not None and task.gpu_preference != 'null':\n                        gpu_has_pending_task.add(task.gpu_preference)\n                        continue\n                    return None, None\n\n        return None, None\n\n    def has_waiting(self, sub_task_list):\n        for sub_task in sub_task_list:\n            if sub_task.status == WAITING:\n                return True\n        return False\n\n    def find_proper_workers(self, sub_task_list, all_res=False):\n        nodes = self.get_all_nodes()\n        if nodes is None or len(nodes) == 0:\n            self.logger.warning('[task_scheduler] running nodes not found')\n            return None\n\n        proper_workers = []\n        has_waiting = False\n        for sub_task in sub_task_list:\n            if sub_task.status == WAITING:\n                has_waiting = True\n            if sub_task.worker is not None and sub_task.vnode_started:\n                proper_workers.append(sub_task.worker)\n                continue\n            needs = sub_task.vnode_info.vnode.instance\n            self.logger.info('sub_task %s-%d' %(sub_task.root_task.id, sub_task.vnode_info.vnodeid))\n            self.logger.info(str(needs))\n            #logger.info(needs)\n            proper_worker = None\n            for worker_ip, worker_info in nodes:\n                self.logger.info('worker ip' + worker_ip)\n                self.logger.info('cpu usage: ' + str(self.get_cpu_usage(worker_ip)))\n                self.logger.info('gpu usage: ' + str(self.get_gpu_usage(worker_ip)))\n                self.logger.info('worker_info: ' + str(worker_info))\n                #logger.info(worker_info)\n                #logger.info(self.get_cpu_usage(worker_ip))\n                if needs.gpu > 0 and sub_task.gpu_preference is not None and sub_task.gpu_preference != 'null' and sub_task.gpu_preference != worker_info['gpu_name']:\n                    continue\n                if needs.cpu + (not all_res) * self.get_cpu_usage(worker_ip) > worker_info['cpu']:\n                    continue\n                elif needs.memory > worker_info['memory']:\n                    continue\n                elif needs.disk > worker_info['disk']:\n                    continue\n                # try not to assign non-gpu task to a worker with gpu\n                #if needs['gpu'] == 0 and worker_info['gpu'] > 0:\n                    #continue\n                elif needs.gpu + (not all_res) * self.get_gpu_usage(worker_ip) > worker_info['gpu']:\n                    continue\n                else:\n                    worker_info['cpu'] -= needs.cpu\n                    worker_info['memory'] -= needs.memory\n                    worker_info['gpu'] -= needs.gpu\n                    worker_info['disk'] -= needs.disk\n                    proper_worker = worker_ip\n                    break\n            if proper_worker is not None:\n                proper_workers.append(proper_worker)\n            else:\n                return []\n        if has_waiting:\n            return proper_workers\n        else:\n            return []\n\n    def get_all_nodes(self):\n        # cache running nodes\n        # if self.all_nodes is not None and time.time() - self.last_nodes_info_update_time < self.nodes_info_update_interval:\n        #     return self.all_nodes\n        # get running nodes\n        node_ips = self.nodemgr.get_batch_nodeips()\n        all_nodes = [(node_ip, self.get_worker_resource_info(node_ip)) for node_ip in node_ips]\n        return all_nodes\n\n    def is_alive(self, worker):\n        nodes = self.nodemgr.get_batch_nodeips()\n        return worker in nodes\n\n    def get_worker_resource_info(self, worker_ip):\n        fetcher = self.monitor_fetcher(worker_ip)\n        worker_info = fetcher.info\n        info = {}\n        info['cpu'] = len(worker_info['cpuconfig'])\n        info['memory'] = (worker_info['meminfo']['buffers'] + worker_info['meminfo']['cached'] + worker_info['meminfo']['free']) / 1024 # (Mb)\n        info['disk'] = sum([disk['free'] for disk in worker_info['diskinfo']]) / 1024 / 1024 # (Mb)\n        info['gpu'] = len(worker_info['gpuinfo'])\n        info['gpu_name'] = worker_info['gpuinfo'][0]['name'] if len(worker_info['gpuinfo']) > 0 else ''\n        info['gpu_price'] = worker_info['gpuinfo'][0]['price'] if len(worker_info['gpuinfo']) > 0 else 0\n        return info\n\n    def get_cpu_usage(self, worker_ip):\n        try:\n            return self.cpu_usage[worker_ip]\n        except:\n            self.cpu_usage[worker_ip] = 0\n            return 0\n\n\n    def get_gpu_usage(self, worker_ip):\n        try:\n            return self.gpu_usage[worker_ip]\n        except:\n            self.gpu_usage[worker_ip] = 0\n            return 0\n\n    # save the task information into database\n    # called when jobmgr assign task to taskmgr\n    @data_lock('add_lock')\n    def add_task(self, username, taskid, json_task, task_priority=1):\n        # decode json string to object defined in grpc\n        self.logger.info('[taskmgr add_task] receive task %s' % taskid)\n\n        image_dict = {\n            \"private\": Image.PRIVATE,\n            \"base\": Image.BASE,\n            \"public\": Image.PUBLIC\n        }\n        max_size = (1 << self.task_cidr) - 2\n        if int(json_task['vnodeCount']) > max_size:\n            # tell jobmgr\n            self.jobmgr.report(username,taskid,\"failed\",\"vnodeCount exceed limits.\")\n            return False\n        task = Task(\n            taskmgr = self,\n            task_id = taskid,\n            username = username,\n            # all vnode must be started at the same time\n            at_same_time = 'atSameTime' in json_task.keys(),\n            priority = task_priority,\n            max_size = (1 << self.task_cidr) - 2,\n            task_infos = [{\n                'gpu_preference': json_task['gpuPreference'] if int(json_task['gpuSetting']) > 0 else 'null',\n                'max_retry_count': int(json_task['retryCount']),\n                'vnode_info': VNodeInfo(\n                    taskid = taskid,\n                    username = username,\n                    vnode = VNode(\n                        image = Image(\n                            name = '_'.join(json_task['image'].split('_')[:-2]), #json_task['cluster']['image']['name'],\n                            type = image_dict[json_task['image'].split('_')[-1]], #json_task['cluster']['image']['type'],\n                            owner = username if not json_task['image'].split('_')[-2] else json_task['image'].split('_')[-2]), #json_task['cluster']['image']['owner']),\n                        instance = Instance(\n                            cpu = int(json_task['cpuSetting']),\n                            memory = int(json_task['memorySetting']),\n                            disk = int(json_task['diskSetting']),\n                            gpu = int(json_task['gpuSetting'])),\n                        mount = [Mount(\n                                    provider = json_task['mapping'][mapping_key]['mappingProvider'],\n                                    localPath = json_task['mapping'][mapping_key]['mappingMountpath'],\n                                    remotePath = json_task['mapping'][mapping_key]['mappingBucketName'],\n                                    accessKey = json_task['mapping'][mapping_key]['mappingAccessKey'],\n                                    secretKey = json_task['mapping'][mapping_key]['mappingSecretKey'],\n                                    other = json_task['mapping'][mapping_key]['mappingEndpoint']\n                                    )\n                                for mapping_key in json_task['mapping']] if 'mapping' in json_task else []\n                        ),\n                ),\n                'command_info': TaskInfo(\n                    taskid = taskid,\n                    username = username,\n                    parameters = Parameters(\n                        command = Command(\n                            commandLine = json_task['command'],\n                            packagePath = json_task['srcAddr'],\n                            envVars = {}),\n                        stderrRedirectPath = json_task.get('stdErrRedPth',\"\"),\n                        stdoutRedirectPath = json_task.get('stdOutRedPth',\"\")),\n                    timeout = int(json_task['expTime'])\n                # commands are executed in all vnodes / only excuted in the first vnode\n                # if in traditional mode, commands will be executed in all vnodes\n                ) if (json_task['runon'] == 'all' or vnode_index == 0) else None\n            } for vnode_index in range(int(json_task['vnodeCount']))])\n\n        if task.at_same_time:\n            workers = self.find_proper_workers(task.subtask_list, all_res=True)\n            if len(workers) == 0:\n                task.status = FAILED\n                # tell jobmgr\n                self.jobmgr.report(username,taskid,\"failed\",\"Resources needs exceed limits\")\n                return False\n        else:\n            for sub_task in task.subtask_list:\n                workers = self.find_proper_workers([sub_task], all_res=True)\n                if len(workers) == 0:\n                    task.status = FAILED\n                    # tell jobmgr\n                    self.jobmgr.report(username,taskid,\"failed\",\"Resources needs exceed limits\")\n                    return False\n        self.lazy_append_list.append(task)\n        return True\n\n\n    @data_lock('task_queue_lock')\n    def get_task_list(self):\n        return self.task_queue.copy()\n\n    @data_lock('task_queue_lock')\n    def get_pending_gpu_tasks_info(self):\n        return self.gpu_pending_tasks\n\n    def get_task_order(self, taskid):\n        task = self.get_task(taskid)\n        if task is not None:\n            return task.order\n        return -1\n\n    @data_lock('task_queue_lock')\n    def get_task(self, taskid):\n        for task in self.task_queue:\n            if task.id == taskid:\n                return task\n        return None\n\n\n    def set_jobmgr(self, jobmgr):\n        self.jobmgr = jobmgr\n\n\n    # get names of all the batch containers of the user\n    def get_user_batch_containers(self,username):\n        return []\n        # if not username in self.user_containers.keys():\n        #     return []\n        # else:\n        #     return self.user_containers[username]\n"
  },
  {
    "path": "src/master/testTaskCtrler.py",
    "content": "import sys\nif sys.path[0].endswith(\"master\"):\n    sys.path[0] = sys.path[0][:-6]\n\nimport grpc,time\n\nfrom protos import rpc_pb2, rpc_pb2_grpc\nimport random, string\n\ndef run():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n\n    comm = rpc_pb2.Command(commandLine=\"ls /root;sleep 5;ls /root\", packagePath=\"/root\", envVars={'test1':'10','test2':'20'}) # | awk '{print \\\"test\\\\\\\"\\\\n\\\"}'\n    paras = rpc_pb2.Parameters(command=comm, stderrRedirectPath=\"/root/nfs/batch_{jobid}/\", stdoutRedirectPath=\"/root/nfs/batch_{jobid}/\")\n\n    img = rpc_pb2.Image(name=\"base\", type=rpc_pb2.Image.BASE, owner=\"docklet\")\n    inst = rpc_pb2.Instance(cpu=1, memory=1000, disk=1000, gpu=0)\n    mnt = rpc_pb2.Mount(localPath=\"\",provider='aliyun',remotePath=\"test-for-docklet\",other=\"oss-cn-beijing.aliyuncs.com\",accessKey=\"LTAIdl7gmmIhfqA9\",secretKey=\"\")\n    clu = rpc_pb2.Cluster(image=img, instance=inst, mount=[])\n\n    task = rpc_pb2.TaskInfo(id=\"test\",username=\"root\",instanceid=1,instanceCount=1,maxRetryCount=1,parameters=paras,cluster=clu,timeout=60000,token=''.join(random.sample(string.ascii_letters + string.digits, 8)))\n\n    response = stub.process_task(task)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\ndef stop_task():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n\n    taskmsg = rpc_pb2.TaskMsg(taskid=\"test\",username=\"root\",instanceid=1,instanceStatus=rpc_pb2.COMPLETED,token=\"test\",errmsg=\"\")\n    reportmsg = rpc_pb2.ReportMsg(taskmsgs = [taskmsg])\n\n    response = stub.stop_tasks(reportmsg)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\nif __name__ == '__main__':\n    #for i in range(10):\n    run()\n    #time.sleep(4)\n    #stop_task()\n"
  },
  {
    "path": "src/master/testTaskMgr.py",
    "content": "import master.taskmgr\nfrom concurrent import futures\nimport grpc\nfrom protos.rpc_pb2 import *\nfrom protos.rpc_pb2_grpc import *\nimport threading, json, time, random\nfrom utils import env\n\n\nclass SimulatedNodeMgr():\n\tdef get_batch_nodeips(self):\n\t\treturn ['0.0.0.0']\n\n\nclass SimulatedMonitorFetcher():\n\tdef __init__(self, ip):\n\t\tself.info = {}\n\t\tself.info['cpuconfig'] = [1,1,1,1,1,1,1,1]\n\t\tself.info['meminfo'] = {}\n\t\tself.info['meminfo']['free'] = 8 * 1024 * 1024 # (kb) simulate 8 GB memory\n\t\tself.info['meminfo']['buffers'] = 8 * 1024 * 1024\n\t\tself.info['meminfo']['cached'] = 8 * 1024 * 1024\n\t\tself.info['diskinfo'] = []\n\t\tself.info['diskinfo'].append({})\n\t\tself.info['diskinfo'][0]['free'] = 16 * 1024 * 1024 * 1024 # (b) simulate 16 GB disk\n\t\tself.info['gpuinfo'] = [1,1]\n\n\nclass SimulatedTaskController(WorkerServicer):\n\n\tdef __init__(self, worker):\n\t\tself.worker = worker\n\n\tdef start_vnode(self, vnodeinfo, context):\n\t\tprint('[SimulatedTaskController] start vnode, taskid [%s] vnodeid [%d]' % (vnodeinfo.taskid, vnodeinfo.vnodeid))\n\t\treturn Reply(status=Reply.ACCEPTED,message=\"\")\n\t\n\tdef stop_vnode(self, vnodeinfo, context):\n\t\tprint('[SimulatedTaskController] stop vnode, taskid [%s] vnodeid [%d]' % (vnodeinfo.taskid, vnodeinfo.vnodeid))\n\t\treturn Reply(status=Reply.ACCEPTED,message=\"\")\n\n\tdef start_task(self, taskinfo, context):\n\t\tprint('[SimulatedTaskController] start task, taskid [%s] vnodeid [%d] token [%s]' % (taskinfo.taskid, taskinfo.vnodeid, taskinfo.token))\n\t\tworker.process(taskinfo)\n\t\treturn Reply(status=Reply.ACCEPTED,message=\"\")\n\n\tdef stop_task(self, taskinfo, context):\n\t\tprint('[SimulatedTaskController] stop task, taskid [%s] vnodeid [%d] token [%s]' % (taskinfo.taskid, taskinfo.vnodeid, taskinfo.token))\n\t\treturn Reply(status=Reply.ACCEPTED,message=\"\")\n\n\nclass SimulatedWorker(threading.Thread):\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.thread_stop = False\n\t\tself.tasks = []\n\n\tdef run(self):\n\t\tworker_port = env.getenv('BATCH_WORKER_PORT')\n\t\tserver = grpc.server(futures.ThreadPoolExecutor(max_workers=5))\n\t\tadd_WorkerServicer_to_server(SimulatedTaskController(self), server)\n\t\tserver.add_insecure_port('[::]:' + worker_port)\n\t\tserver.start()\n\t\twhile not self.thread_stop:\n\t\t\tfor task in self.tasks:\n\t\t\t\tseed = random.random()\n\t\t\t\tif seed < 0.25:\n\t\t\t\t\treport(task.taskid, task.vnodeid, RUNNING, task.token)\n\t\t\t\telif seed < 0.5:\n\t\t\t\t\treport(task.taskid, task.vnodeid, COMPLETED, task.token)\n\t\t\t\t\tself.tasks.remove(task)\n\t\t\t\t\tbreak\n\t\t\t\telif seed < 0.75:\n\t\t\t\t\treport(task.taskid, task.vnodeid, FAILED, task.token)\n\t\t\t\t\tself.tasks.remove(task)\n\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tpass\n\t\t\ttime.sleep(5)\n\t\tserver.stop(0)\n\n\tdef stop(self):\n\t\tself.thread_stop = True\n\n\tdef process(self, task):\n\t\tself.tasks.append(task)\n\n\nclass SimulatedJobMgr(threading.Thread):\n\n\tdef __init__(self):\n\t\tthreading.Thread.__init__(self)\n\t\tself.thread_stop = False\n\n\tdef run(self):\n\t\twhile not self.thread_stop:\n\t\t\ttime.sleep(5)\n\t\tserver.stop(0)\n\n\tdef stop(self):\n\t\tself.thread_stop = True\n\n\tdef report(self, task):\n\t\tprint('[SimulatedJobMgr] task[%s] status %d' % (task.info.id, task.status))\n\n\tdef assignTask(self, taskmgr, taskid, instance_count, retry_count, timeout, cpu, memory, disk, gpu):\n\t\ttask = {}\n\t\ttask['instCount'] = instance_count\n\t\ttask['retryCount'] = retry_count\n\t\ttask['expTime'] = timeout\n\t\ttask['at_same_time'] = True\n\t\ttask['multicommand'] = True\n\t\ttask['command'] = 'ls'\n\t\ttask['srcAddr'] = ''\n\t\ttask['envVars'] = {'a':'1'}\n\t\ttask['stdErrRedPth'] = ''\n\t\ttask['stdOutRedPth'] = ''\n\t\ttask['image'] = 'root_root_base'\n\t\ttask['cpuSetting'] = cpu\n\t\ttask['memorySetting'] = memory\n\t\ttask['diskSetting'] = disk\n\t\ttask['gpuSetting'] = 0\n\t\ttask['mapping'] = []\n\n\t\ttaskmgr.add_task('root', taskid, task)\n\n\nclass SimulatedLogger():\n\tdef info(self, msg):\n\t\tprint('[INFO]    ' + msg)\n\n\tdef warning(self, msg):\n\t\tprint('[WARNING] ' + msg)\n\n\tdef error(self, msg):\n\t\tprint('[ERROR]   ' + msg)\n\n\ndef test():\n\tglobal worker\n\tglobal jobmgr\n\tglobal taskmgr\n\n\tworker = SimulatedWorker()\n\tworker.start()\n\tjobmgr = SimulatedJobMgr()\n\tjobmgr.start()\n\n\ttaskmgr = master.taskmgr.TaskMgr(SimulatedNodeMgr(), SimulatedMonitorFetcher, master_ip='', scheduler_interval=2, external_logger=SimulatedLogger())\n\t# taskmgr.set_jobmgr(jobmgr)\n\ttaskmgr.start()\n\n\tadd('task_0', instance_count=2, retry_count=2, timeout=60, cpu=2, memory=2048, disk=2048, gpu=0)\n\n\ndef test2():\n\tglobal jobmgr\n\tglobal taskmgr\n\tjobmgr = SimulatedJobMgr()\n\tjobmgr.start()\n\n\ttaskmgr = master.taskmgr.TaskMgr(SimulatedNodeMgr(), SimulatedMonitorFetcher, master_ip='', scheduler_interval=2, external_logger=SimulatedLogger())\n\ttaskmgr.set_jobmgr(jobmgr)\n\ttaskmgr.start()\n\n\tadd('task_0', instance_count=2, retry_count=2, timeout=60, cpu=2, memory=2048, disk=2048, gpu=0)\n\n\n\ndef add(taskid, instance_count, retry_count, timeout, cpu, memory, disk, gpu):\n\tglobal jobmgr\n\tglobal taskmgr\n\tjobmgr.assignTask(taskmgr, taskid, instance_count, retry_count, timeout, cpu, memory, disk, gpu)\n\n\ndef report(taskid, instanceid, status, token):\n\tglobal taskmgr\n\n\tmaster_port = env.getenv('BATCH_MASTER_PORT')\n\tchannel = grpc.insecure_channel('%s:%s' % ('0.0.0.0', master_port))\n\tstub = MasterStub(channel)\n\tresponse = stub.report(ReportMsg(taskmsgs=[TaskMsg(taskid=taskid, username='root', vnodeid=instanceid, subTaskStatus=status, token=token)]))\n\n\ndef stop():\n\tglobal worker\n\tglobal jobmgr\n\tglobal taskmgr\n\n\tworker.stop()\n\tjobmgr.stop()\n\ttaskmgr.stop()\n"
  },
  {
    "path": "src/master/testTaskWorker.py",
    "content": "import sys\nif sys.path[0].endswith(\"master\"):\n    sys.path[0] = sys.path[0][:-6]\n\nimport grpc,time\n\nfrom protos import rpc_pb2, rpc_pb2_grpc\nimport random, string\n\ndef run():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n\n    #comm = rpc_pb2.Command(commandLine=\"ls /root;sleep 5;ls /root\", packagePath=\"/root\", envVars={'test1':'10','test2':'20'}) # | awk '{print \\\"test\\\\\\\"\\\\n\\\"}'\n    #paras = rpc_pb2.Parameters(command=comm, stderrRedirectPath=\"/root/nfs/batch_{jobid}/\", stdoutRedirectPath=\"/root/nfs/batch_{jobid}/\")\n\n    img = rpc_pb2.Image(name=\"base\", type=rpc_pb2.Image.BASE, owner=\"docklet\")\n    inst = rpc_pb2.Instance(cpu=1, memory=1000, disk=1000, gpu=0)\n    mnt = rpc_pb2.Mount(localPath=\"\",provider='aliyun',remotePath=\"test-for-docklet\",other=\"oss-cn-beijing.aliyuncs.com\",accessKey=\"LTAIdl7gmmIhfqA9\",secretKey=\"\")\n    network = rpc_pb2.Network(ipaddr=\"10.0.4.2/24\",gateway=\"10.0.4.1\",masterip=\"192.168.0.1\",brname=\"batch-root-test\")\n    vnode = rpc_pb2.VNode(image=img, instance=inst, mount=[],network=network,hostname=\"batch-5\")\n    vnodeinfo = rpc_pb2.VNodeInfo(taskid=\"test\",username=\"root\",vnodeid=1,vnode=vnode)\n\n    #task = rpc_pb2.TaskInfo(id=\"test\",username=\"root\",instanceid=1,instanceCount=1,maxRetryCount=1,parameters=paras,cluster=clu,timeout=60000,token=''.join(random.sample(string.ascii_letters + string.digits, 8)))\n\n    response = stub.start_vnode(vnodeinfo)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\ndef stop_task():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n\n    taskmsg = rpc_pb2.TaskMsg(taskid=\"test\",username=\"root\",instanceid=1,instanceStatus=rpc_pb2.COMPLETED,token=\"test\",errmsg=\"\")\n    reportmsg = rpc_pb2.ReportMsg(taskmsgs = [taskmsg])\n\n    response = stub.stop_tasks(reportmsg)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\ndef stop_vnode():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n    network = rpc_pb2.Network(brname=\"batch-root-test\")\n    vnodeinfo = rpc_pb2.VNodeInfo(taskid=\"test\",username=\"root\",vnodeid=1,vnode=rpc_pb2.VNode(network=network))\n\n    response = stub.stop_vnode(vnodeinfo)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\ndef start_task():\n    channel = grpc.insecure_channel('localhost:50051')\n    stub = rpc_pb2_grpc.WorkerStub(channel)\n\n    comm = rpc_pb2.Command(commandLine=\"ls /root;sleep 5;ls /root\", packagePath=\"/root\", envVars={'test1':'10','test2':'20'}) # | awk '{print \\\"test\\\\\\\"\\\\n\\\"}'\n    paras = rpc_pb2.Parameters(command=comm, stderrRedirectPath=\"/root/nfs/batch_{jobid}/\", stdoutRedirectPath=\"/root/nfs/batch_{jobid}/\")\n    taskinfo = rpc_pb2.TaskInfo(taskid=\"test\",username=\"root\",vnodeid=1,parameters=paras,timeout=20,token=\"test\")\n\n    response = stub.start_task(taskinfo)\n    print(\"Batch client received: \" + str(response.status)+\" \"+response.message)\n\n\nif __name__ == '__main__':\n    #for i in range(10):\n    #run()\n    #start_task()\n    stop_vnode()\n    #time.sleep(4)\n    #stop_task()\n"
  },
  {
    "path": "src/master/userManager.py",
    "content": "'''\nuserManager for Docklet\nprovide a class for managing users and usergroups in Docklet\nWarning: in some early versions, \"token\" stand for the instance of class model.User\n         now it stands for a string that can be parsed to get that instance.\n         in all functions start with \"@administration_required\" or \"@administration_or_self_required\", \"token\" is the instance\nOriginal author: Liu Peidong\n'''\n\nfrom utils.model import db, User, UserGroup, Notification, UserUsage, LoginMsg, LoginFailMsg\nfrom functools import wraps\nimport os, subprocess, math\nimport hashlib\nimport pam\nfrom base64 import b64encode\nfrom utils import env\nfrom master.settings import settings\nimport smtplib\nfrom email.mime.text import MIMEText\nfrom email.mime.multipart import MIMEMultipart\nfrom email.header import Header\nfrom datetime import datetime, timedelta\nimport json\nfrom utils.log import logger\nfrom utils.lvmtool import *\n\nPAM = pam.pam()\nfspath = env.getenv('FS_PREFIX')\ndata_quota = env.getenv('DATA_QUOTA')\ndata_quota_cmd = env.getenv('DATA_QUOTA_CMD')\n\n\nif (env.getenv('EXTERNAL_LOGIN').lower() == 'true'):\n    from plugin import external_receive\n\ndef administration_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if ( ('cur_user' in kwargs) == False):\n            return {\"success\":'false', \"reason\":\"Cannot get cur_user\"}\n        cur_user = kwargs['cur_user']\n        if ((cur_user.user_group == 'admin') or (cur_user.user_group == 'root')):\n            return func(*args, **kwargs)\n        else:\n            return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n\n    return wrapper\n\ndef administration_or_self_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if ( (not ('cur_user' in kwargs)) or (not ('user' in kwargs))):\n            return {\"success\":'false', \"reason\":\"Cannot get cur_user or user\"}\n        cur_user = kwargs['cur_user']\n        user = kwargs['user']\n        if ((cur_user.user_group == 'admin') or (cur_user.user_group == 'root') or (cur_user.username == user.username)):\n            return func(*args, **kwargs)\n        else:\n            return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n\n    return wrapper\n\ndef token_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if ( ('cur_user' in kwargs) == False):\n            return {\"success\":'false', \"reason\":\"Cannot get cur_user\"}\n        return func(*args, **kwargs)\n\n    return wrapper\n\ndef send_activated_email(to_address, username):\n    email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n    if (email_from_address in ['\\'\\'', '\\\"\\\"', '']):\n        return\n    #text = 'Dear '+ username + ':\\n' + '  Your account in docklet has been activated'\n    text = '<html><h4>Dear '+ username + ':</h4>'\n    text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Your account in <a href='%s'>%s</a> has been activated</p>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Enjoy your personal workspace in the cloud !</p>\n               <br>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Note: DO NOT reply to this email!</p>\n               <br><br>\n               <p> <a href='http://docklet.unias.org'>Docklet Team</a>, SEI, PKU</p>\n            ''' % (env.getenv(\"PORTAL_URL\"), env.getenv(\"PORTAL_URL\"))\n    text += '<p>'+  str(datetime.now()) + '</p>'\n    text += '</html>'\n    subject = 'Docklet account activated'\n    msg = MIMEMultipart()\n    textmsg = MIMEText(text,'html','utf-8')\n    msg['Subject'] = Header(subject, 'utf-8')\n    msg['From'] = email_from_address\n    msg['To'] = to_address\n    msg.attach(textmsg)\n    s = smtplib.SMTP()\n    s.connect()\n    s.sendmail(email_from_address, to_address, msg.as_string())\n    s.close()\n\ndef send_remind_activating_email(username):\n    #admin_email_address = env.getenv('ADMIN_EMAIL_ADDRESS')\n    nulladdr = ['\\'\\'', '\\\"\\\"', '']\n    email_from_address = settings.get('EMAIL_FROM_ADDRESS')\n    admin_email_address = settings.get('ADMIN_EMAIL_ADDRESS')\n    if (email_from_address in nulladdr or admin_email_address in nulladdr):\n        return\n    #text = 'Dear '+ username + ':\\n' + '  Your account in docklet has been activated'\n    text = '<html><h4>Dear '+ 'admin' + ':</h4>'\n    text += '''<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;An activating request for %s in <a href='%s'>%s</a> has been sent</p>\n               <p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Please check it !</p>\n               <br/><br/>\n               <p> Docklet Team, SEI, PKU</p>\n            ''' % (username, env.getenv(\"PORTAL_URL\"), env.getenv(\"PORTAL_URL\"))\n    text += '<p>'+  str(datetime.utcnow()) + '</p>'\n    text += '</html>'\n    subject = 'An activating request in Docklet has been sent'\n    if admin_email_address[0] == '\"':\n        admins_addr = admin_email_address[1:-1].split(\" \")\n    else:\n        admins_addr = admin_email_address.split(\" \")\n    alladdr=\"\"\n    for addr in admins_addr:\n        alladdr = alladdr+addr+\", \"\n    alladdr=alladdr[:-2]\n    msg = MIMEMultipart()\n    textmsg = MIMEText(text,'html','utf-8')\n    msg['Subject'] = Header(subject, 'utf-8')\n    msg['From'] = email_from_address\n    msg['To'] = alladdr\n    msg.attach(textmsg)\n    s = smtplib.SMTP()\n    s.connect()\n    try:\n        s.sendmail(email_from_address, admins_addr, msg.as_string())\n    except Exception as e:\n        logger.error(e)\n    s.close()\n\n\nclass userManager:\n    def __init__(self, username = 'root', password = None):\n        '''\n        Try to create the database when there is none\n        initialize 'root' user and 'root' & 'primary' group\n        '''\n        try:\n            User.query.all()\n        except:\n            db.create_all()\n            if password == None:\n                #set a random password\n                password = os.urandom(16)\n                password = b64encode(password).decode('utf-8')\n                fsdir = env.getenv('FS_PREFIX')\n                f = open(fsdir + '/local/generated_password.txt', 'w')\n                f.write(\"User=%s\\nPass=%s\\n\"%(username, password))\n                f.close()\n            sys_admin = User(username, hashlib.sha512(password.encode('utf-8')).hexdigest())\n            sys_admin.status = 'normal'\n            sys_admin.nickname = 'root'\n            sys_admin.description = 'Root_User'\n            sys_admin.user_group = 'root'\n            sys_admin.auth_method = 'local'\n            db.session.add(sys_admin)\n            path = env.getenv('DOCKLET_LIB')\n            subprocess.call([path+\"/master/userinit.sh\", username])\n            db.session.commit()\n        if not os.path.exists(fspath+\"/global/sys/quota\"):\n            groupfile = open(fspath+\"/global/sys/quota\",'w')\n            groups = []\n            groups.append({'name':'root', 'quotas':{ 'cpu':'4', 'disk':'2000', 'data':'100', 'memory':'2000', 'image':'10', 'idletime':'24', 'vnode':'8', 'portmapping': '8', 'input_rate_limit':'10000', 'output_rate_limit':'10000'}})\n            groups.append({'name':'admin', 'quotas':{'cpu':'4', 'disk':'2000', 'data':'100', 'memory':'2000', 'image':'10', 'idletime':'24', 'vnode':'8', 'portmapping': '8', 'input_rate_limit':'10000', 'output_rate_limit':'10000'}})\n            groups.append({'name':'primary', 'quotas':{'cpu':'4', 'disk':'2000', 'data':'100', 'memory':'2000', 'image':'10', 'idletime':'24', 'vnode':'8', 'portmapping': '8', 'input_rate_limit':'10000', 'output_rate_limit':'10000'}})\n            groups.append({'name':'foundation', 'quotas':{'cpu':'4', 'disk':'2000', 'data':'100', 'memory':'2000', 'image':'10', 'idletime':'24', 'vnode':'8', 'portmapping': '8', 'input_rate_limit':'10000', 'output_rate_limit':'10000'}})\n            groupfile.write(json.dumps(groups))\n            groupfile.close()\n        if not os.path.exists(fspath+\"/global/sys/quotainfo\"):\n            quotafile = open(fspath+\"/global/sys/quotainfo\",'w')\n            quotas = {}\n            quotas['default'] = 'foundation'\n            quotas['quotainfo'] = []\n            quotas['quotainfo'].append({'name':'cpu', 'hint':'the cpu quota, number of cores, e.g. 4'})\n            quotas['quotainfo'].append({'name':'memory', 'hint':'the memory quota, number of MB , e.g. 4000'})\n            quotas['quotainfo'].append({'name':'disk', 'hint':'the disk quota, number of MB, e.g. 4000'})\n            quotas['quotainfo'].append({'name':'data', 'hint':'the quota of data space, number of GB, e.g. 100'})\n            quotas['quotainfo'].append({'name':'image', 'hint':'how many images the user can save, e.g. 10'})\n            quotas['quotainfo'].append({'name':'idletime', 'hint':'will stop cluster after idletime, number of hours, e.g. 24'})\n            quotas['quotainfo'].append({'name':'vnode', 'hint':'how many containers the user can have, e.g. 8'})\n            quotas['quotainfo'].append({'name':'portmapping', 'hint':'how many ports the user can map, e.g. 8'})\n            quotas['quotainfo'].append({'name':'input_rate_limit', 'hint':'the ingress speed of the network, number of kbps. 0 means the rate are unlimited.'})\n            quotas['quotainfo'].append({'name':'output_rate_limit', 'hint':'the egress speed of the network, number of kbps. 0 means the rate are unlimited.'})\n            quotafile.write(json.dumps(quotas))\n            quotafile.close()\n        if not os.path.exists(fspath+\"/global/sys/lxc.default\"):\n            settingfile = open(fspath+\"/global/sys/lxc.default\", 'w')\n            settings = {}\n            settings['cpu'] = \"2\"\n            settings[\"memory\"] = \"2000\"\n            settings[\"disk\"] = \"2000\"\n            settingfile.write(json.dumps(settings))\n            settingfile.close()\n\n        try:\n            UserUsage.query.all()\n            LoginMsg.query.all()\n            LoginFailMsg.query.all()\n        except:\n            db.create_all()\n\n    def auth_local(self, username, password):\n        password = hashlib.sha512(password.encode('utf-8')).hexdigest()\n        user = User.query.filter_by(username = username).first()\n        if (user == None):\n            return {\"success\":'false', \"reason\": \"User does not exist!\"}\n        if (user.password != password):\n            return {\"success\":'false', \"reason\": \"Wrong password!\"}\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"group\" : user.user_group,\n                \"token\" : user.generate_auth_token(),\n            }\n        }\n        return result\n\n    def auth_pam(self, username, password):\n        user = User.query.filter_by(username = username).first()\n        pamresult = PAM.authenticate(username, password)\n        if (pamresult == False or (user != None and user.auth_method != 'pam')):\n            return {\"success\":'false', \"reason\": \"Wrong password or wrong login method!\"}\n        if (user == None):\n            newuser = self.newuser();\n            newuser.username = username\n            newuser.password = \"no_password\"\n            newuser.nickname = username\n            newuser.status = \"init\"\n            newuser.user_group = \"primary\"\n            newuser.auth_method = \"pam\"\n            self.register(user = newuser)\n            user = User.query.filter_by(username = username).first()\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"group\" : user.user_group,\n                \"token\" : user.generate_auth_token(),\n            }\n        }\n        return result\n\n    def auth_external(self, form, userip=\"\"):\n\n        if (env.getenv('EXTERNAL_LOGIN') != 'True'):\n            failed_result = {'success': 'false', 'reason' : 'external auth disabled'}\n            return failed_result\n\n        result = external_receive.external_auth_receive_request(form)\n\n        if (result['success'] != 'True'):\n            failed_result =  {'success':'false',  'result': result}\n            return failed_result\n\n        username = result['username']\n        logger.info(\"External login success: username=%s, userip=%s\" % (username, userip))\n        loginmsg = LoginMsg(username,userip)\n        db.session.add(loginmsg)\n        db.session.commit()\n        user = User.query.filter_by(username = username).first()\n        if (user != None and user.auth_method == result['auth_method']):\n            result = {\n                \"success\": 'true',\n                \"data\":{\n                    \"username\" : user.username,\n                    \"avatar\" : user.avatar,\n                    \"nickname\" : user.nickname,\n                    \"description\" : user.description,\n                    \"status\" : user.status,\n                    \"group\" : user.user_group,\n                    \"token\" : user.generate_auth_token(),\n                }\n            }\n            return result\n        if (user != None and user.auth_method != result['auth_method']):\n            result = {'success': 'false', 'reason': 'other kinds of account already exists'}\n            return result\n        #user == None , register an account for external user\n        newuser = self.newuser();\n        newuser.username = result['username']\n        newuser.password = result['password']\n        newuser.avatar = result['avatar']\n        newuser.nickname = result['nickname']\n        newuser.description = result['description']\n        newuser.e_mail = result['e_mail']\n        newuser.truename = result['truename']\n        newuser.student_number = result['student_number']\n        newuser.status = result['status']\n        newuser.user_group = result['user_group']\n        newuser.auth_method = result['auth_method']\n        newuser.department = result['department']\n        newuser.tel = result['tel']\n        self.register(user = newuser)\n        user = User.query.filter_by(username = username).first()\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"group\" : user.user_group,\n                \"token\" : user.generate_auth_token(),\n            }\n        }\n        return result\n\n    def auth(self, username, password, userip=\"\"):\n        '''\n        authenticate a user by username & password\n        return a token as well as some user information\n        '''\n        user = User.query.filter_by(username = username).first()\n        failmsg = LoginFailMsg.query.filter_by(username = username).first()\n        result = {}\n        if failmsg == None:\n            newfailmsg = LoginFailMsg(username)\n            db.session.add(newfailmsg)\n            db.session.commit()\n            failmsg = newfailmsg\n        elif failmsg.failcnt > 40:\n            reason = \"You have been input wrong password over 40 times. You account will be locked. Please contact administrators for help.\"\n            logger.info(\"Login failed: userip=%s reason:%s\" % (userip,reason))\n            return {'success':'false', 'reason':reason}\n        elif datetime.now() < failmsg.bantime:\n            reason = \"You have been input wrong password %d times. Please try after %s.\" % (failmsg.failcnt, failmsg.bantime.strftime(\"%Y-%m-%d %H:%M:%S\"))\n            logger.info(\"Login failed: userip=%s reason:%s\" % (userip,reason))\n            return {'success':'false', 'reason':reason}\n\n        if (user == None or user.auth_method =='local'):\n            result = self.auth_local(username, password)\n        elif (user.auth_method == 'pam'):\n            result = self.auth_pam(username, password)\n        else:\n            result  = {'success':'false', 'reason':'auth_method error!'}\n\n        if result['success'] == 'true':\n            loginmsg = LoginMsg(result['data']['username'],userip)\n            failmsg.failcnt = 0\n            db.session.add(loginmsg)\n            db.session.commit()\n            logger.info(\"Login success: username=%s, userip=%s\" % (result['data']['username'], userip))\n        else:\n            logger.info(\"Login failed: userip=%s\" % (userip))\n            failmsg.failcnt += 1\n            if failmsg.failcnt == 5:\n                failmsg.bantime = datetime.now() + timedelta(minutes=5)\n            elif failmsg.failcnt == 10:\n                failmsg.bantime = datetime.now() + timedelta(minutes=10)\n            elif failmsg.failcnt == 20:\n                failmsg.bantime = datetime.now() + timedelta(minutes=100)\n            elif failmsg.failcnt == 30:\n                failmsg.bantime = datetime.now() + timedelta(days=1)\n            db.session.commit()\n        return result\n\n    def auth_token(self, token):\n        '''\n        authenticate a user by a token\n        when succeeded, return the database iterator\n        otherwise return None\n        '''\n        user = User.verify_auth_token(token)\n        return user\n\n    def set_nfs_quota_bygroup(self,groupname, quota):\n        if not data_quota == \"True\":\n            return\n        users = User.query.filter_by(user_group = groupname).all()\n        for user in users:\n            self.set_nfs_quota(user.username, quota)\n\n    def set_nfs_quota(self, username, quota):\n        if not data_quota == \"True\":\n            return\n        nfspath = \"/users/%s/data\" % username\n        try:\n            cmd = data_quota_cmd % (nfspath,quota+\"GB\")\n            sys_run(cmd.strip('\"'))\n        except Exception as e:\n            logger.error(e)\n\n\n    @administration_required\n    def query(*args, **kwargs):\n        '''\n        Usage: query(username = 'xxx', cur_user = token_from_auth)\n            || query(ID = a_integer, cur_user = token_from_auth)\n        Provide information about one user that administrators need to use\n        '''\n        if ( 'ID' in kwargs):\n            user = User.query.filter_by(id = kwargs['ID']).first()\n            if (user == None):\n                return {\"success\":False, \"reason\":\"User does not exist\"}\n            result = {\n                \"success\":'true',\n                \"data\":{\n                    \"username\" : user.username,\n                    \"id\": user.id,\n                    \"password\" : user.password,\n                    \"avatar\" : user.avatar,\n                    \"nickname\" : user.nickname,\n                    \"description\" : user.description,\n                    \"status\" : user.status,\n                    \"e_mail\" : user.e_mail,\n                    \"student_number\": user.student_number,\n                    \"department\" : user.department,\n                    \"truename\" : user.truename,\n                    \"tel\" : user.tel,\n                    \"register_date\" : \"%s\"%(user.register_date),\n                    \"group\" : user.user_group,\n                    \"description\" : user.description,\n                    \"beans\" : user.beans,\n                },\n                \"token\": user\n            }\n            return result\n\n        if ( 'username' not in kwargs):\n            return {\"success\":'false', \"reason\":\"Cannot get 'username'\"}\n        username = kwargs['username']\n        user = User.query.filter_by(username = username).first()\n        if (user == None):\n            return {\"success\":'false', \"reason\":\"User does not exist\"}\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"id\": user.id,\n                \"password\" : user.password,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"e_mail\" : user.e_mail,\n                \"student_number\": user.student_number,\n                \"department\" : user.department,\n                \"truename\" : user.truename,\n                \"tel\" : user.tel,\n                \"register_date\" : \"%s\"%(user.register_date),\n                \"group\" : user.user_group,\n                \"beans\" : user.beans,\n            },\n            \"token\": user\n        }\n        return result\n\n    @token_required\n    def selfQuery(*args, **kwargs):\n        '''\n        Usage: selfQuery(cur_user = token_from_auth)\n        List informantion for oneself\n        '''\n        user = kwargs['cur_user']\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        group = None\n        for one_group in groups:\n            if one_group['name'] == user.user_group:\n                group = one_group['quotas']\n                break\n        else:\n            for one_group in groups:\n                if one_group['name'] == \"primary\":\n                    group = one_group['quotas']\n                    break\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"id\": user.id,\n                \"password\" : user.password,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"e_mail\" : user.e_mail,\n                \"student_number\": user.student_number,\n                \"department\" : user.department,\n                \"truename\" : user.truename,\n                \"tel\" : user.tel,\n                \"register_date\" : \"%s\"%(user.register_date),\n                \"group\" : user.user_group,\n                \"groupinfo\": group,\n                \"beans\" : user.beans,\n                \"auth_method\": user.auth_method,\n            },\n        }\n        return result\n\n    @token_required\n    def selfModify(*args, **kwargs):\n        '''\n        Usage: selfModify(cur_user = token_from_auth, newValue = form)\n        Modify informantion for oneself\n        '''\n        form = kwargs['newValue']\n        name = form.get('name', None)\n        value = form.get('value', None)\n        if (name == None or value == None):\n            result = {'success': 'false'}\n            return result\n        user = User.query.filter_by(username = kwargs['cur_user'].username).first()\n        if (name == 'nickname'):\n            user.nickname = value\n        elif (name == 'description'):\n            user.description = value\n        elif (name == 'department'):\n            user.department = value\n        elif (name == 'e_mail'):\n            user.e_mail = value\n        elif (name == 'tel'):\n            user.tel = value\n        elif (name == 'password'):\n            old_password = hashlib.sha512(form.get('old_value', '').encode('utf-8')).hexdigest()\n            if (user.password != old_password):\n                result = {'success': 'false'}\n                return result\n            user.password = hashlib.sha512(value.encode('utf-8')).hexdigest()\n        else:\n            result = {'success': 'false'}\n            return result\n        db.session.commit()\n        result = {'success': 'true'}\n        return result\n\n    @token_required\n    def usageQuery(self, *args, **kwargs):\n        '''\n        Usage: usageQuery(cur_user = token_from_auth)\n        Query the quota and usage of user\n        '''\n        cur_user = kwargs['cur_user']\n        groupname = cur_user.user_group\n        groupinfo = self.groupQuery(name = groupname)['data']\n        usage = UserUsage.query.filter_by(username = cur_user.username).first()\n        if usage == None:\n            new_usage = UserUsage(cur_user.username)\n            db.session.add(new_usage)\n            db.session.commit()\n            usageinfo = {\n                    'username': cur_user.username,\n                    'cpu': '0',\n                    'memory': '0',\n                    'disk': '0'\n                    }\n        else:\n            usageinfo = {\n                    'username': usage.username,\n                    'cpu': usage.cpu,\n                    'memory': usage.memory,\n                    'disk': usage.disk\n                    }\n        settingfile = open(fspath+\"/global/sys/lxc.default\" , 'r')\n        defaultsetting = json.loads(settingfile.read())\n        settingfile.close()\n\n        return {'success': 'true', 'quota' : groupinfo, 'usage' : usageinfo, 'default': defaultsetting }\n\n    @token_required\n    def usageInc(self, *args, **kwargs):\n        '''\n        Usage: usageModify(cur_user = token_from_auth, modification = data_from_form)\n        Modify the usage info of user\n        '''\n        cur_user = kwargs['cur_user']\n        modification = kwargs['modification']\n        logger.info(\"record usage for user:%s\" % cur_user.username)\n        groupname = cur_user.user_group\n        groupinfo = self.groupQuery(name = groupname)['data']\n        usage = UserUsage.query.filter_by(username = cur_user.username).first()\n        if usage == None:\n            new_usage = UserUsage(cur_user.username)\n            db.session.add(new_usage)\n            db.session.commit()\n            usage = UserUsage.query.filter_by(username = cur_user.username).first()\n        if int(modification['cpu']) <= 0 or int(modification['memory']) <= 0 or int(modification['disk']) <= 0:\n            return {'success':False, 'result':\"cpu,memory and disk setting cannot less than zero\"}\n        cpu = int(usage.cpu) + int(modification['cpu'])\n        memory = int(usage.memory) + int(modification['memory'])\n        disk = int(usage.disk) + int(modification['disk'])\n        if cpu > int(groupinfo['cpu']):\n            logger.error(\"cpu quota exceed, user:%s\" % cur_user.username)\n            return {'success':False, 'result':\"cpu quota exceed\"}\n        if memory > int(groupinfo['memory']):\n            logger.error(\"memory quota exceed, user:%s\" % cur_user.username)\n            return {'success':False, 'result':\"memory quota exceed\"}\n        if disk > int(groupinfo['disk']):\n            logger.error(\"disk quota exceed, user:%s\" % cur_user.username)\n            return {'success':False, 'result':\"disk quota exceed\"}\n        usage.cpu = str(cpu)\n        usage.memory = str(memory)\n        usage.disk = str(disk)\n        db.session.commit()\n        return {'success':True, 'result':\"distribute the resource\"}\n\n    @token_required\n    def usageRecover(self, *args, **kwargs):\n        '''\n        Usage: usageModify(cur_user = token_from_auth, modification = data_from_form)\n        Recover the usage info when create container failed\n        '''\n        cur_user = kwargs['cur_user']\n        modification = kwargs['modification']\n        logger.info(\"recover usage for user:%s\" % cur_user.username)\n        usage = UserUsage.query.filter_by(username = cur_user.username).first()\n        if usage == None:\n            new_usage = UserUsage(cur_user.username)\n            db.session.add(new_usage)\n            db.session.commit()\n            usage = UserUsage.query.filter_by(username = cur_user.username).first()\n            return True\n        cpu = int(usage.cpu) - int(modification['cpu'])\n        memory = int(usage.memory) - int(modification['memory'])\n        disk = int(usage.disk) - int(modification['disk'])\n        if cpu < 0:\n            cpu = 0\n        if memory < 0:\n            memory = 0\n        if disk < 0:\n            disk = 0\n        usage.cpu = str(cpu)\n        usage.memory = str(memory)\n        usage.disk = str(disk)\n        db.session.commit()\n        return {'success':True}\n\n    @token_required\n    def usageRelease(self, *args, **kwargs):\n        cur_user = kwargs['cur_user']\n        cpu = kwargs['cpu']\n        memory = kwargs['memory']\n        disk = kwargs['disk']\n        usage = UserUsage.query.filter_by(username = cur_user.username).first()\n        if usage == None:\n            new_usage = UserUsage(cur_user.username)\n            db.session.add(new_usage)\n            db.session.commit()\n            return {'success':True}\n        nowcpu = int(usage.cpu) - int(cpu)\n        nowmemory = int(usage.memory) - int(memory)\n        nowdisk = int(usage.disk) - int(disk)\n        if nowcpu < 0:\n            nowcpu = 0\n        if nowmemory < 0:\n            nowmemory = 0\n        if nowdisk < 0:\n            nowdisk = 0\n        usage.cpu = str(nowcpu)\n        usage.memory = str(nowmemory)\n        usage.disk = str(nowdisk)\n        db.session.commit()\n        return {'success':True}\n\n    def initUsage(*args, **kwargs):\n        \"\"\"\n        init the usage info when start docklet with init mode\n        \"\"\"\n        usages = UserUsage.query.all()\n        for usage in usages:\n            usage.cpu = \"0\"\n            usage.memory = \"0\"\n            usage.disk = \"0\"\n        db.session.commit()\n        return True\n\n    @administration_required\n    def userList(*args, **kwargs):\n        '''\n        Usage: list(cur_user = token_from_auth)\n        List all users for an administrator\n        '''\n        alluser = User.query.all()\n        result = {\n            \"success\": 'true',\n            \"data\":[]\n        }\n        for user in alluser:\n            userinfo = [\n                    user.id,\n                    user.username,\n                    user.truename,\n                    user.e_mail,\n                    user.tel,\n                    \"%s\"%(user.register_date),\n                    user.status,\n                    user.user_group,\n                    user.beans,\n                    '',\n            ]\n            result[\"data\"].append(userinfo)\n        return result\n\n    @administration_required\n    def groupList(*args, **kwargs):\n        '''\n        Usage: list(cur_user = token_from_auth)\n        List all groups for an administrator\n        '''\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'r')\n        quotas = json.loads(quotafile.read())\n        quotafile.close()\n        result = {\n            \"success\": 'true',\n            \"groups\": groups,\n            \"quotas\": quotas['quotainfo'],\n            \"default\": quotas['default'],\n        }\n        return result\n\n    @administration_required\n    def change_default_group(*args, **kwargs):\n        form = kwargs['form']\n        default_group = form.get('defaultgroup')\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'r')\n        quotas = json.loads(quotafile.read())\n        quotafile.close()\n        quotas['default'] = default_group\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'w')\n        quotafile.write(json.dumps(quotas))\n        quotafile.close()\n        return { 'success':'true', 'action':'change default group' }\n\n\n    def groupQuery(self, *args, **kwargs):\n        '''\n        Usage: groupQuery(name = XXX, cur_user = token_from_auth)\n        List a group for an administrator\n        '''\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        for group in groups:\n            if group['name'] == kwargs['name']:\n                result = {\n                    \"success\":'true',\n                    \"data\": group['quotas'],\n                }\n                return result\n        else:\n            return {\"success\":False, \"reason\":\"Group does not exist\"}\n\n    @administration_required\n    def groupListName(*args, **kwargs):\n        '''\n        Usage: grouplist(cur_user = token_from_auth)\n        List all group names for an administrator\n        '''\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        result = {\n            \"groups\": [],\n        }\n        for group in groups:\n            result[\"groups\"].append(group['name'])\n        return result\n\n    @administration_required\n    def groupModify(self, *args, **kwargs):\n        '''\n        Usage: groupModify(newValue = dict_from_form, cur_user = token_from_auth)\n        '''\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        for group in groups:\n            if group['name'] == kwargs['newValue'].get('groupname',None):\n                form = kwargs['newValue']\n                for key in form.keys():\n                    if key == \"data\":\n                        if not group['quotas'][key] == form.get(key):\n                            self.set_nfs_quota_bygroup(group['name'],form.get(key))\n                    else:\n                        pass\n\n                    if key == \"groupname\" or key == \"token\":\n                        pass\n                    else:\n                        if key == \"vnode\":\n                            vnode = int(form['vnode'])\n                            val = str(2**(round(math.log(vnode+3, 2))) - 3 )\n                            group[\"quotas\"][key] = val\n                        else:\n                            group['quotas'][key] = form.get(key)\n                groupfile = open(fspath+\"/global/sys/quota\",'w')\n                groupfile.write(json.dumps(groups))\n                groupfile.close()\n                return {\"success\":'true'}\n        else:\n            return {\"success\":'false', \"reason\":\"UserGroup does not exist\"}\n\n    @administration_required\n    def modify(self, *args, **kwargs):\n        '''\n        modify a user's information in database\n        will send an e-mail when status is changed from 'applying' to 'normal'\n        Usage: modify(newValue = dict_from_form, cur_user = token_from_auth)\n        '''\n        if ( kwargs['newValue'].get('Instruction', '') == 'Activate'):\n            user_modify = User.query.filter_by(id = kwargs['newValue'].get('ID', None)).first()\n            user_modify.status = 'normal'\n            send_activated_email(user_modify.e_mail, user_modify.username)\n            db.session.commit()\n            return {\"success\": \"true\"}\n\n        if ( kwargs['newValue'].get('password', '') != ''):\n            user_modify = User.query.filter_by(username = kwargs['newValue'].get('username', None)).first()\n            new_password = kwargs['newValue'].get('password','')\n            new_password = hashlib.sha512(new_password.encode('utf-8')).hexdigest()\n            user_modify.password = new_password\n            db.session.commit()\n            return {\"success\": \"true\"}\n\n        user_modify = User.query.filter_by(username = kwargs['newValue'].get('username', None)).first()\n        if (user_modify == None):\n\n            return {\"success\":'false', \"reason\":\"User does not exist\"}\n\n        #try:\n        form = kwargs['newValue']\n        user_modify.truename = form.get('truename', '')\n        user_modify.e_mail = form.get('e_mail', '')\n        user_modify.department = form.get('department', '')\n        user_modify.student_number = form.get('student_number', '')\n        user_modify.tel = form.get('tel', '')\n        user_modify.user_group = form.get('group', '')\n        user_modify.auth_method = form.get('auth_method', '')\n        if (user_modify.status == 'applying' and form.get('status', '') == 'normal'):\n            send_activated_email(user_modify.e_mail, user_modify.username)\n        user_modify.status = form.get('status', '')\n        #if (form.get('password', '') != ''):\n            #new_password = form.get('password','')\n            #new_password = hashlib.sha512(new_password.encode('utf-8')).hexdigest()\n            #user_modify.password = new_password\n            #self.chpassword(cur_user = user_modify, password = form.get('password','no_password'))\n        #modify password in another function now\n\n        db.session.commit()\n        res = self.groupQuery(name=user_modify.user_group)\n        if res['success']:\n            self.set_nfs_quota(user_modify.username,res['data']['data'])\n        return {\"success\":'true'}\n        #except:\n            #return {\"success\":'false', \"reason\":\"Something happened\"}\n\n    @token_required\n    def chpassword(*args, **kwargs):\n        '''\n        Usage: chpassword(cur_user = token_from_auth, password = 'your_password')\n        '''\n        cur_user = kwargs['cur_user']\n        cur_user.password = hashlib.sha512(kwargs['password'].encode('utf-8')).hexdigest()\n\n\n\n    def newuser(*args, **kwargs):\n        '''\n        Usage : newuser()\n        The only method to create a new user\n        call this method first, modify the return value which is a database row instance,then call self.register()\n        '''\n        user_new = User('newuser', 'asdf1234')\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'r')\n        quotas = json.loads(quotafile.read())\n        quotafile.close()\n        user_new.user_group = quotas['default']\n        user_new.avatar = 'default.png'\n        return user_new\n\n    def register(self, *args, **kwargs):\n        '''\n        Usage: register(user = modified_from_newuser())\n        '''\n\n        if (kwargs['user'].username == None or kwargs['user'].username == ''):\n            return {\"success\":'false', \"reason\": \"Empty username\"}\n        user_check = User.query.filter_by(username = kwargs['user'].username).first()\n        if (user_check != None and user_check.status != \"init\"):\n            #for the activating form\n            return {\"success\":'false', \"reason\": \"Unauthorized action\"}\n        newuser = kwargs['user']\n        if (user_check != None and (user_check.status == \"init\")):\n            db.session.delete(user_check)\n            db.session.commit()\n        else:\n            newuser.password = hashlib.sha512(newuser.password.encode('utf-8')).hexdigest()\n        db.session.add(newuser)\n        db.session.commit()\n\n        # if newuser status is normal, init some data for this user\n        # now initialize for all kind of users\n        #if newuser.status == 'normal':\n        path = env.getenv('DOCKLET_LIB')\n        subprocess.call([path+\"/master/userinit.sh\", newuser.username])\n        res = self.groupQuery(name=newuser.user_group)\n        if res['success']:\n            self.set_nfs_quota(newuser.username,res['data']['data'])\n        return {\"success\":'true'}\n\n    @administration_required\n    def quotaadd(*args, **kwargs):\n        form = kwargs.get('form')\n        quotaname = form.get(\"quotaname\")\n        default_value = form.get(\"default_value\")\n        hint = form.get(\"hint\")\n        if (quotaname == None):\n            return { \"success\":'false', \"reason\": \"Empty quota name\"}\n        if (default_value == None):\n            default_value = \"--\"\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        for group in groups:\n            group['quotas'][quotaname] = default_value\n        groupfile = open(fspath+\"/global/sys/quota\",'w')\n        groupfile.write(json.dumps(groups))\n        groupfile.close()\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'r')\n        quotas = json.loads(quotafile.read())\n        quotafile.close()\n        quotas['quotainfo'].append({'name':quotaname, 'hint':hint})\n        quotafile = open(fspath+\"/global/sys/quotainfo\",'w')\n        quotafile.write(json.dumps(quotas))\n        quotafile.close()\n        return {\"success\":'true'}\n\n    @administration_required\n    def groupadd(*args, **kwargs):\n        form = kwargs.get('form')\n        groupname = form.get(\"groupname\")\n        if (groupname == None):\n            return {\"success\":'false', \"reason\": \"Empty group name\"}\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        group = {\n            'name': groupname,\n            'quotas': {}\n        }\n        for key in form.keys():\n            if key == \"groupname\" or key == \"token\":\n                pass\n            else:\n                if key == \"vnode\":\n                    vnode = int(form['vnode'])\n                    val = str(2**(round(math.log(vnode+3, 2))) - 3 )\n                    group['quotas'][key] = val\n                else:\n                    group['quotas'][key] = form.get(key)\n        groups.append(group)\n        groupfile = open(fspath+\"/global/sys/quota\",'w')\n        groupfile.write(json.dumps(groups))\n        groupfile.close()\n        return {\"success\":'true'}\n\n    @administration_required\n    def groupdel(*args, **kwargs):\n        name = kwargs.get('name', None)\n        if (name == None):\n            return {\"success\":'false', \"reason\": \"Empty group name\"}\n        groupfile = open(fspath+\"/global/sys/quota\",'r')\n        groups = json.loads(groupfile.read())\n        groupfile.close()\n        for group in groups:\n            if group['name'] == name:\n                groups.remove(group)\n                break\n        groupfile = open(fspath+\"/global/sys/quota\",'w')\n        groupfile.write(json.dumps(groups))\n        groupfile.close()\n        return {\"success\":'true'}\n\n    @administration_required\n    def lxcsettingList(*args, **kwargs):\n        lxcsettingfile = open(fspath+\"/global/sys/lxc.default\", 'r')\n        lxcsetting = json.loads(lxcsettingfile.read())\n        lxcsettingfile.close()\n        return {\"success\": 'true', 'data':lxcsetting}\n\n    @administration_required\n    def chlxcsetting(*args, **kwargs):\n        form = kwargs['form']\n        lxcsetting = {}\n        lxcsetting['cpu'] = form['lxcCpu']\n        lxcsetting['memory'] = form['lxcMemory']\n        lxcsetting['disk'] = form['lxcDisk']\n        lxcsettingfile = open(fspath+\"/global/sys/lxc.default\", 'w')\n        lxcsettingfile.write(json.dumps(lxcsetting))\n        lxcsettingfile.close()\n        return {\"success\": 'true'}\n\n\n    def queryForDisplay(*args, **kwargs):\n        '''\n        Usage: queryForDisplay(user = token_from_auth)\n        Provide information about one user that administrators need to use\n        '''\n\n        if ( 'user' not in kwargs):\n            return {\"success\":'false', \"reason\":\"Cannot get 'user'\"}\n        user = kwargs['user']\n        if (user == None):\n            return {\"success\":'false', \"reason\":\"User does not exist\"}\n        result = {\n            \"success\": 'true',\n            \"data\":{\n                \"username\" : user.username,\n                \"password\" : user.password,\n                \"avatar\" : user.avatar,\n                \"nickname\" : user.nickname,\n                \"description\" : user.description,\n                \"status\" : user.status,\n                \"e_mail\" : user.e_mail,\n                \"student_number\": user.student_number,\n                \"department\" : user.department,\n                \"truename\" : user.truename,\n                \"tel\" : user.tel,\n                \"register_date\" : \"%s\"%(user.register_date),\n                \"group\" : user.user_group,\n                \"auth_method\": user.auth_method,\n            }\n        }\n        return result\n\n#    def usermodify(rowID, columnID, newValue, cur_user):\n#        '''not used now'''\n#        user = um.query(ID = request.form[\"rowID\"], cur_user = root).get('token',  None)\n#        result = um.modify(user = user, columnID = request.form[\"columnID\"], newValue = request.form[\"newValue\"], cur_user = root)\n#        return json.dumps(result)\n"
  },
  {
    "path": "src/master/userinit.sh",
    "content": "#!/bin/bash\n\n# initialize for a new user\n#        initialize directory : clusters, data, ssh\n#        generate ssh keys for new user\n\n[ -z $FS_PREFIX ] && FS_PREFIX=\"/opt/docklet\"\n\nUSERNAME=$1\n\n[ -z $USERNAME ] && echo \"[userinit.sh] USERNAME is needed\" && exit 1 \n\necho \"[Info] [userinit.sh] initialize for user $USERNAME\"\n\nUSER_DIR=$FS_PREFIX/global/users/$USERNAME\n[ -d $USER_DIR ] && echo \"[userinit.sh] user directory already exists\" && exit 0\n\nmkdir -p $USER_DIR/{clusters,hosts,data,ssh}\n\nSSH_DIR=$USER_DIR/ssh\n# here generate id_rsa.pub has \"user@hostname\" at the end\n# maybe it should be delete\nssh-keygen -t rsa -P '' -f $SSH_DIR/id_rsa &>/dev/null\ncp $SSH_DIR/id_rsa.pub $SSH_DIR/authorized_keys\n\ncat << EOF > $SSH_DIR/config\nHost *\n    StrictHostKeyChecking no\n    UserKnownHostsFile=/dev/null\nEOF\n"
  },
  {
    "path": "src/master/vclustermgr.py",
    "content": "#!/usr/bin/python3\n\nimport os, random, json, sys\nimport datetime, math, time\n\nfrom utils.log import logger\nfrom utils import env, imagemgr, proxytool\nimport requests, threading, traceback\nfrom utils.nettools import portcontrol\nfrom utils.model import db, Container, PortMapping, VCluster\nfrom queue import Queue\n\nuserpoint = \"http://\" + env.getenv('USER_IP') + \":\" + str(env.getenv('USER_PORT'))\ndef post_to_user(url = '/', data={}):\n    return requests.post(userpoint+url,data=data).json()\n\n##################################################\n#                  VclusterMgr\n# Description : VclusterMgr start/stop/manage virtual clusters\n#\n##################################################\n\ndef db_commit():\n    try:\n        db.session.commit()\n    except Exception as err:\n        db.session.rollback()\n        logger.error(traceback.format_exc())\n        return False\n    return True\n\nclass VclusterMgr(object):\n    def __init__(self, nodemgr, networkmgr, etcdclient, addr, mode, distributedgw='False'):\n        self.mode = mode\n        self.distributedgw = distributedgw\n        self.nodemgr = nodemgr\n        self.imgmgr = imagemgr.ImageMgr()\n        self.networkmgr = networkmgr\n        self.addr = addr\n        self.etcd = etcdclient\n        self.defaultsize = env.getenv(\"CLUSTER_SIZE\")\n        self.fspath = env.getenv(\"FS_PREFIX\")\n        self.clusterid_locks = threading.Lock()\n\n        # check database\n        try:\n            Container.query.all()\n            PortMapping.query.all()\n            VCluster.query.all()\n        except:\n            # create database\n            db.create_all()\n\n        logger.info (\"vcluster start on %s\" % (self.addr))\n        if self.mode == 'new':\n            logger.info (\"starting in new mode on %s\" % (self.addr))\n            # check if all clusters data are deleted in httprest.py\n            clean = True\n            usersdir = self.fspath+\"/global/users/\"\n            vclusters = VCluster.query.all()\n            if len(vclusters) != 0:\n                clean = False\n            for user in os.listdir(usersdir):\n                if len(os.listdir(usersdir+user+\"/hosts\")) > 0:\n                    clean = False\n            if not clean:\n                logger.error (\"clusters files not clean, start failed\")\n                sys.exit(1)\n        elif self.mode == \"recovery\":\n            logger.info (\"starting in recovery mode on %s\" % (self.addr))\n            self.recover_allclusters()\n        else:\n            logger.error (\"not supported mode:%s\" % self.mode)\n            sys.exit(1)\n        # start a thread to watch recovering node to recover clusters\n        threading.Thread(target = self._watchrecovering, args=()).start()\n\n    def _watchrecovering(self):\n        logger.info(\"watching recovering node to recover clusters...\")\n        while True:\n            node = self.nodemgr.recover_queue.get()\n            self.recover_cluster_on(node)\n            self.nodemgr.recover_queue.task_done()\n    \n    def recover_allclusters(self):\n        logger.info(\"try to recover all clusters for all users...\")\n        vcs = VCluster.query.filter(VCluster.status.in_([\"running\",\"error\"])).all()\n        need_recovers = []\n        for vc in vcs:\n            need_recovers.append([vc.clustername, vc.ownername])\n        self.recover_clusters(need_recovers)        \n\n    def mount_allclusters(self):\n        logger.info(\"mounting all vclusters for all users...\")\n        usersdir = self.fspath+\"/global/users/\"\n        for user in os.listdir(usersdir):\n            for cluster in self.list_clusters(user)[1]:\n                logger.info (\"mounting cluster:%s for user:%s ...\" % (cluster, user))\n                self.mount_cluster(cluster, user)\n        logger.info(\"mounted all vclusters for all users\")\n\n    def stop_allclusters(self):\n        logger.info(\"stopping all vclusters for all users...\")\n        usersdir = self.fspath+\"/global/users/\"\n        for user in os.listdir(usersdir):\n            for cluster in self.list_clusters(user)[1]:\n                logger.info (\"stopping cluster:%s for user:%s ...\" % (cluster, user))\n                self.stop_cluster(cluster, user)\n        logger.info(\"stopped all vclusters for all users\")\n\n    def detach_allclusters(self):\n        logger.info(\"detaching all vclusters for all users...\")\n        usersdir = self.fspath+\"/global/users/\"\n        for user in os.listdir(usersdir):\n            for cluster in self.list_clusters(user)[1]:\n                logger.info (\"detaching cluster:%s for user:%s ...\" % (cluster, user))\n                self.detach_cluster(cluster, user)\n        logger.info(\"detached all vclusters for all users\")\n\n    def create_cluster(self, clustername, username, image, user_info, setting):\n        if self.is_cluster(clustername, username):\n            return [False, \"cluster:%s already exists\" % clustername]\n        if self.imgmgr.get_image_size(image) + 100 > int(setting[\"disk\"]):\n            return [False, \"the size of disk is not big enough for the image\"]\n        clustersize = int(self.defaultsize)\n        logger.info (\"starting cluster %s with %d containers for %s\" % (clustername, int(clustersize), username))\n        workers = self.nodemgr.get_base_nodeips()\n        image_json = json.dumps(image)\n        groupname = json.loads(user_info)[\"data\"][\"group\"]\n        groupquota = json.loads(user_info)[\"data\"][\"groupinfo\"]\n        uid = json.loads(user_info)[\"data\"][\"id\"]\n        if (len(workers) == 0):\n            logger.warning (\"no workers to start containers, start cluster failed\")\n            return [False, \"no workers are running\"]\n        # check user IP pool status, should be moved to user init later\n        if not self.networkmgr.has_user(username):\n            ipnum = int(groupquota[\"vnode\"]) + 3\n            cidr = 32 - math.ceil(math.log(ipnum,2))\n            self.networkmgr.add_user(username, cidr=cidr, isshared = True if str(groupname) == \"fundation\" else False)\n            if self.distributedgw == \"False\":\n                [success,message] = self.networkmgr.setup_usrgw(groupquota['input_rate_limit'], groupquota['output_rate_limit'], username, uid, self.nodemgr)\n                if not success:\n                    return [False, message]\n        elif not self.networkmgr.has_usrgw(username):\n            self.networkmgr.usrgws[username] = self.networkmgr.masterip\n            self.networkmgr.dump_usrgw(username)\n        [status, result] = self.networkmgr.acquire_userips_cidr(username, clustersize)\n        gateway = self.networkmgr.get_usergw(username)\n        #vlanid = self.networkmgr.get_uservlanid(username)\n        logger.info (\"create cluster with gateway : %s\" % gateway)\n        self.networkmgr.printpools()\n        if not status:\n            logger.info (\"create cluster failed: %s\" % result)\n            return [False, result]\n        ips = result\n        clusterid = self._acquire_id()\n        clusterpath = self.fspath+\"/global/users/\"+username+\"/clusters/\"+clustername\n        hostpath = self.fspath+\"/global/users/\"+username+\"/hosts/\"+str(clusterid)+\".hosts\"\n        hosts = \"127.0.0.1\\tlocalhost\\n\"\n        proxy_server_ip = \"\"\n        proxy_public_ip = \"\"\n        containers = []\n        for i in range(0, clustersize):\n            workerip = workers[random.randint(0, len(workers)-1)]\n            oneworker = self.nodemgr.ip_to_rpc(workerip)\n            if self.distributedgw == \"True\" and i == 0 and not self.networkmgr.has_usrgw(username):\n                [success,message] = self.networkmgr.setup_usrgw(groupquota['input_rate_limit'], groupquota['output_rate_limit'], username, uid, self.nodemgr, workerip)\n                if not success:\n                    return [False, message]\n            if i == 0:\n                self.networkmgr.load_usrgw(username)\n                proxy_server_ip = self.networkmgr.usrgws[username]\n                [status, proxy_public_ip] = self.etcd.getkey(\"machines/publicIP/\"+proxy_server_ip)\n                if not status:\n                    logger.error(\"Fail to get proxy_public_ip %s.\"%(proxy_server_ip))\n                    return [False, \"Fail to get proxy server public IP.\"]\n            lxc_name = username + \"-\" + str(clusterid) + \"-\" + str(i)\n            hostname = \"host-\"+str(i)\n            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))\n            [success,message] = oneworker.create_container(lxc_name, proxy_public_ip, username, uid, json.dumps(setting) , clustername, str(clusterid), str(i), hostname, ips[i], gateway, image_json)\n            if success is False:\n                self.networkmgr.release_userips(username, ips[i])\n                logger.info(\"container create failed, so vcluster create failed\")\n                return [False, message]\n            logger.info(\"container create success\")\n            hosts = hosts + ips[i].split(\"/\")[0] + \"\\t\" + hostname + \"\\t\" + hostname + \".\"+clustername + \"\\n\"\n            containers.append(Container(lxc_name,hostname,ips[i],workerip,image['name'],datetime.datetime.now(),setting))\n            #containers.append({ 'containername':lxc_name, 'hostname':hostname, 'ip':ips[i], 'host':workerip, 'image':image['name'], 'lastsave':datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"), 'setting': setting })\n        hostfile = open(hostpath, 'w')\n        hostfile.write(hosts)\n        hostfile.close()\n        #clusterfile = open(clusterpath, 'w')\n        vcluster = VCluster(clusterid,clustername,username,'stopped',clustersize,clustersize,proxy_server_ip,proxy_public_ip)\n        for con in containers:\n            vcluster.containers.append(con)\n        db.session.add(vcluster)\n        db_commit()\n        #proxy_url = env.getenv(\"PORTAL_URL\") +\"/\"+ proxy_public_ip +\"/_web/\" + username + \"/\" + clustername\n        #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':\"------\"}\n        #info['proxy_url'] = proxy_url\n        #info['proxy_server_ip'] = proxy_server_ip\n        #info['proxy_public_ip'] = proxy_public_ip\n        #info['port_mapping'] = []\n        #clusterfile.write(json.dumps(info))\n        #clusterfile.close()\n        return [True, str(vcluster)]\n\n    def scale_out_cluster(self,clustername,username, image,user_info, setting):\n        if not self.is_cluster(clustername,username):\n            return [False, \"cluster:%s not found\" % clustername]\n        if self.imgmgr.get_image_size(image) + 100 > int(setting[\"disk\"]):\n            return [False, \"the size of disk is not big enough for the image\"]\n        workers = self.nodemgr.get_base_nodeips()\n        if (len(workers) == 0):\n            logger.warning(\"no workers to start containers, scale out failed\")\n            return [False, \"no workers are running\"]\n        image_json = json.dumps(image)\n        [status, result] = self.networkmgr.acquire_userips_cidr(username)\n        gateway = self.networkmgr.get_usergw(username)\n        #vlanid = self.networkmgr.get_uservlanid(username)\n        self.networkmgr.printpools()\n        if not status:\n            return [False, result]\n        ip = result[0]\n        [status, clusterinfo] = self.get_clusterinfo(clustername,username)\n        clusterid = clusterinfo['clusterid']\n        clusterpath = self.fspath + \"/global/users/\" + username + \"/clusters/\" + clustername\n        hostpath = self.fspath + \"/global/users/\" + username + \"/hosts/\" + str(clusterid) + \".hosts\"\n        cid = clusterinfo['nextcid']\n        workerip = workers[random.randint(0, len(workers)-1)]\n        oneworker = self.nodemgr.ip_to_rpc(workerip)\n        lxc_name = username + \"-\" + str(clusterid) + \"-\" + str(cid)\n        hostname = \"host-\" + str(cid)\n        proxy_server_ip = clusterinfo['proxy_server_ip']\n        proxy_public_ip = clusterinfo['proxy_public_ip']\n        uid = json.loads(user_info)[\"data\"][\"id\"]\n        [success, message] = oneworker.create_container(lxc_name, proxy_public_ip, username, uid, json.dumps(setting), clustername, clusterid, str(cid), hostname, ip, gateway, image_json)\n        if success is False:\n            self.networkmgr.release_userips(username, ip)\n            logger.info(\"create container failed, so scale out failed\")\n            return [False, message]\n        if clusterinfo['status'] == \"running\":\n            self.networkmgr.check_usergre(username, uid, workerip, self.nodemgr, self.distributedgw=='True')\n            oneworker.start_container(lxc_name)\n            oneworker.start_services(lxc_name, [\"ssh\"]) # TODO: need fix\n            namesplit = lxc_name.split('-')\n            portname = namesplit[1] + '-' + namesplit[2]\n            oneworker.recover_usernet(portname, uid, proxy_server_ip, workerip==proxy_server_ip)\n        logger.info(\"scale out success\")\n        hostfile = open(hostpath, 'a')\n        hostfile.write(ip.split(\"/\")[0] + \"\\t\" + hostname + \"\\t\" + hostname + \".\" + clustername + \"\\n\")\n        hostfile.close()\n        [success,vcluster] = self.get_vcluster(clustername,username)\n        if not success:\n            return [False, \"Fail to write info.\"]\n        vcluster.nextcid = int(clusterinfo['nextcid']) + 1\n        vcluster.size = int(clusterinfo['size']) + 1\n        vcluster.containers.append(Container(lxc_name,hostname,ip,workerip,image['name'],datetime.datetime.now(),setting))\n        #{'containername':lxc_name, 'hostname':hostname, 'ip':ip, 'host':workerip, 'image':image['name'], 'lastsave':datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\"), 'setting': setting})\n        db.session.add(vcluster)\n        db_commit()\n        return [True, clusterinfo]\n\n    def addproxy(self,username,clustername,ip,port):\n        [status, clusterinfo] = self.get_clusterinfo(clustername, username)\n        if 'proxy_ip' in clusterinfo:\n            return [False, \"proxy already exists\"]\n        target = \"http://\" + ip + \":\" + port + \"/\"\n        clusterinfo['proxy_ip'] = ip + \":\" + port\n        if self.distributedgw == 'True':\n            worker = self.nodemgr.ip_to_rpc(clusterinfo['proxy_server_ip'])\n            worker.set_route(\"/\"+ clusterinfo['proxy_public_ip'] + \"/_web/\" + username + \"/\" + clustername, target)\n        else:\n            proxytool.set_route(\"/\" + clusterinfo['proxy_public_ip'] + \"/_web/\" + username + \"/\" + clustername, target)\n        clusterfile = open(self.fspath + \"/global/users/\" + username + \"/clusters/\" + clustername, 'w')\n        clusterfile.write(json.dumps(clusterinfo))\n        clusterfile.close()\n        return [True, clusterinfo]\n\n    def deleteproxy(self, username, clustername):\n        [status, clusterinfo] = self.get_clusterinfo(clustername, username)\n        if 'proxy_ip' not in clusterinfo:\n            return [True, clusterinfo]\n        clusterinfo.pop('proxy_ip')\n        if self.distributedgw == 'True':\n            worker = self.nodemgr.ip_to_rpc(clusterinfo['proxy_server_ip'])\n            worker.delete_route(\"/\" + clusterinfo['proxy_public_ip'] + \"/_web/\" + username + \"/\" + clustername)\n        else:\n            proxytool.delete_route(\"/\" + clusterinfo['proxy_public_ip'] + \"/_web/\" + username + \"/\" + clustername)\n        clusterfile = open(self.fspath + \"/global/users/\" + username + \"/clusters/\" + clustername, 'w')\n        clusterfile.write(json.dumps(clusterinfo))\n        clusterfile.close()\n        return [True, clusterinfo]\n\n    def count_port_mapping(self, username):\n        return sum([len(self.get_clusterinfo(cluster, username)[1]['port_mapping']) for cluster in self.list_clusters(username)[1]])\n\n    def add_port_mapping(self,username,clustername,node_name,node_ip,port,quota):\n        port_mapping_count = self.count_port_mapping(username)\n\n        if port_mapping_count >= int(quota['portmapping']):\n            return [False, 'Port mapping quota exceed.']\n\n        [status, clusterinfo] = self.get_clusterinfo(clustername, username)\n        if clusterinfo['status'] == 'stopped':\n            return [False, 'Please start the clusters first.']\n        host_port = 0\n        if self.distributedgw == 'True':\n            worker = self.nodemgr.ip_to_rpc(clusterinfo['proxy_server_ip'])\n            [success, host_port] = worker.acquire_port_mapping(node_name, node_ip, port)\n        else:\n            [success, host_port] = portcontrol.acquire_port_mapping(node_name, node_ip, port)\n        if not success:\n            return [False, host_port]\n        [status,vcluster] = self.get_vcluster(clustername,username)\n        if not status:\n            return [False,\"VCluster not found.\"]\n        vcluster.port_mapping.append(PortMapping(node_name,node_ip,port,host_port))\n        db.session.add(vcluster)\n        db_commit()\n        return [True, json.loads(str(vcluster))]\n\n    def recover_port_mapping(self,username,clustername):\n        [status, clusterinfo] = self.get_clusterinfo(clustername, username)\n        for rec in clusterinfo['port_mapping']:\n            if self.distributedgw == 'True':\n                worker = self.nodemgr.ip_to_rpc(clusterinfo['proxy_server_ip'])\n                [success, host_port] = worker.acquire_port_mapping(rec['node_name'], rec['node_ip'], rec['node_port'], rec['host_port'])\n            else:\n                [success, host_port] = portcontrol.acquire_port_mapping(rec['node_name'], rec['node_ip'], rec['node_port'], rec['host_port'])\n            if not success:\n                return [False, host_port]\n        return [True, clusterinfo]\n\n    def delete_all_port_mapping(self, username, clustername, node_name):\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        if not status:\n            return [False,\"VCluster not found.\"]\n        error_msg = None\n        delete_list = []\n        for item in vcluster.port_mapping:\n            if item.node_name == node_name:\n                node_ip = item.node_ip\n                node_port = item.node_port\n                if self.distributedgw == 'True':\n                    worker = self.nodemgr.ip_to_rpc(vcluster.proxy_server_ip)\n                    [success,msg] = worker.release_port_mapping(node_name, node_ip, str(node_port))\n                else:\n                    [success,msg] = portcontrol.release_port_mapping(node_name, node_ip, str(node_port))\n                if not success:\n                    error_msg = msg\n                else:\n                    delete_list.append(item)\n        if len(delete_list) > 0:\n            for item in delete_list:\n                db.session.delete(item)\n            db_commit()\n        else:\n            return [True,\"No port mapping.\"]\n        if error_msg is not None:\n            return [False,error_msg]\n        else:\n            return [True,\"Success\"]\n\n    def delete_port_mapping(self, username, clustername, node_name, node_port):\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        if not status:\n            return [False,\"VCluster not found.\"]\n        for item in vcluster.port_mapping:\n            if item.node_name == node_name and str(item.node_port) == str(node_port):\n                node_ip = item.node_ip\n                node_port = item.node_port\n                if self.distributedgw == 'True':\n                    worker = self.nodemgr.ip_to_rpc(vcluster.proxy_server_ip)\n                    [success,msg] = worker.release_port_mapping(node_name, node_ip, str(node_port))\n                else:\n                    [success,msg] = portcontrol.release_port_mapping(node_name, node_ip, str(node_port))\n                db.session.delete(item)\n                db_commit()\n                if success:\n                    return [True, json.loads(str(vcluster))]\n                else:\n                    return [False, msg]\n        return [False, \"No port mapping.\"]\n\n    def flush_cluster(self,username,clustername,containername):\n        begintime = datetime.datetime.now()\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        containers = info['containers']\n        imagetmp = username + \"_tmp_docklet\"\n        for container in containers:\n            if container['containername'] == containername:\n                logger.info(\"container: %s found\" % containername)\n                worker = self.nodemgr.ip_to_rpc(container['host'])\n                worker.create_image(username,imagetmp,containername)\n                fimage = container['image']\n                logger.info(\"image: %s created\" % imagetmp)\n                break\n        else:\n            logger.error(\"container: %s not found\" % containername)\n        for container in containers:\n            if container['containername'] != containername:\n                logger.info(\"container: %s now flush\" % container['containername'])\n                worker = self.nodemgr.ip_to_rpc(container['host'])\n                #t = threading.Thread(target=onework.flush_container,args=(username,imagetmp,container['containername']))\n                #threads.append(t)\n                worker.flush_container(username,imagetmp,container['containername'])\n                container['lastsave'] = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n                container['image'] = fimage\n                logger.info(\"thread for container: %s has been prepared\" % container['containername'])\n        clusterpath = self.fspath + \"/global/users/\" + username + \"/clusters/\" + clustername\n        infofile = open(clusterpath,'w')\n        infofile.write(json.dumps(info))\n        infofile.close()\n        self.imgmgr.removeImage(username,imagetmp)\n        endtime = datetime.datetime.now()\n        dtime = (endtime - begintime).seconds\n        logger.info(\"flush spend %s seconds\" % dtime)\n        logger.info(\"flush success\")\n\n\n    def image_check(self,username,imagename):\n        imagepath = self.fspath + \"/global/images/private/\" + username + \"/\"\n        if os.path.exists(imagepath + imagename):\n            return [False, \"image already exists\"]\n        else:\n            return [True, \"image not exists\"]\n\n    def create_image(self,username,clustername,containername,imagename,description,imagenum=10):\n        [status, vcluster] = self.get_vcluster(clustername,username)\n        if not status:\n            return [False, \"cluster not found\"]\n        containers = vcluster.containers\n        for container in containers:\n            if container.containername == containername:\n                logger.info(\"container: %s found\" % containername)\n                worker = self.nodemgr.ip_to_rpc(container.host)\n                if worker is None:\n                    return [False, \"The worker %s can't be found or has been stopped.\" % container.host]\n                res = worker.create_image(username,imagename,containername,description,imagenum)\n                container.lastsave = datetime.datetime.now()\n                container.image = imagename\n                break\n        else:\n            res = [False, \"container not found\"]\n            logger.error(\"container: %s not found\" % containername)\n        db_commit()\n        return res\n\n    def delete_cluster(self, clustername, username, user_info):\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if vcluster.status =='running':\n            return [False, \"cluster is still running, you need to stop it and then delete\"]\n        ips = []\n        for container in vcluster.containers:\n            worker = self.nodemgr.ip_to_rpc(container.host)\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container.host]\n            worker.delete_container(container.containername)\n            db.session.delete(container)\n            ips.append(container.ip)\n        logger.info(\"delete vcluster and release vcluster ips\")\n        self.networkmgr.release_userips(username, ips)\n        self.networkmgr.printpools()\n        #os.remove(self.fspath+\"/global/users/\"+username+\"/clusters/\"+clustername)\n        for bh in vcluster.billing_history:\n            db.session.delete(bh)\n        db.session.delete(vcluster)\n        db_commit()\n        os.remove(self.fspath+\"/global/users/\"+username+\"/hosts/\"+str(vcluster.clusterid)+\".hosts\")\n\n        groupname = json.loads(user_info)[\"data\"][\"group\"]\n        uid = json.loads(user_info)[\"data\"][\"id\"]\n        [status, clusters] = self.list_clusters(username)\n        if len(clusters) == 0:\n            self.networkmgr.del_user(username)\n            self.networkmgr.del_usrgwbr(username, uid, self.nodemgr)\n            #logger.info(\"vlanid release triggered\")\n\n        return [True, \"cluster delete\"]\n\n    def scale_in_cluster(self, clustername, username, containername):\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        new_containers = []\n        for container in vcluster.containers:\n            if container.containername == containername:\n                worker = self.nodemgr.ip_to_rpc(container.host)\n                if worker is None:\n                    return [False, \"The worker %s can't be found or has been stopped.\" % container.host]\n                worker.delete_container(containername)\n                db.session.delete(container)\n                self.networkmgr.release_userips(username, container.ip)\n                self.networkmgr.printpools()\n        vcluster.size -= 1\n        cid = containername[containername.rindex(\"-\")+1:]\n        clusterid = vcluster.clusterid\n        hostpath = self.fspath + \"/global/users/\" + username + \"/hosts/\" + str(clusterid) + \".hosts\"\n        db_commit()\n        hostfile = open(hostpath, 'r')\n        hostinfo = hostfile.readlines()\n        hostfile.close()\n        hostfile = open(hostpath, 'w')\n        new_hostinfo = []\n        new_hostinfo.append(hostinfo[0])\n        for host in hostinfo[1:]:\n            parts = host.split(\"\\t\")\n            if parts[1][parts[1].rindex(\"-\")+1:] == cid:\n                pass\n            else:\n                new_hostinfo.append(host)\n        hostfile.writelines(new_hostinfo)\n        hostfile.close()\n        [success, msg] = self.delete_all_port_mapping(username, clustername, containername)\n        if not success:\n            return [False, msg]\n        [status, info] = self.get_clusterinfo(clustername, username)\n        return [True, info]\n\n    def get_clustersetting(self, clustername, username, containername, allcontainer):\n        [status,vcluster] = self.get_vcluster(clustername,username)\n        if vcluster is None:\n            logger.error(\"cluster file: %s not found\" % clustername)\n            return [False, \"cluster file not found\"]\n        cpu = 0\n        memory = 0\n        disk = 0\n        if allcontainer:\n            for container in vcluster.containers:\n                cpu += int(container.setting_cpu)\n                memory += int(container.setting_mem)\n                disk += int(container.setting_disk)\n        else:\n            for container in vcluster.containers:\n                if container.containername == containername:\n                    cpu += int(container.setting_cpu)\n                    memory += int(container.setting_mem)\n                    disk += int(container.setting_disk)\n        return [True, {'cpu':cpu, 'memory':memory, 'disk':disk}]\n\n    def update_cluster_baseurl(self, clustername, username, oldip, newip):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        logger.info(\"%s %s:base_url need to be modified(%s %s).\"%(username,clustername,oldip,newip))\n        for container in info['containers']:\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            #if worker is None:\n            #    return [False, \"The worker can't be found or has been stopped.\"]\n            self.nodemgr.call_rpc_function(worker,'update_baseurl',[container['containername'],oldip,newip])\n            self.nodemgr.call_rpc_function(worker,'stop_container',[container['containername']])\n\n    def check_public_ip(self, clustername, username):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        [status, proxy_public_ip] = self.etcd.getkey(\"machines/publicIP/\"+info['proxy_server_ip'])\n        if not info['proxy_public_ip'] == proxy_public_ip:\n            logger.info(\"%s %s proxy_public_ip has been changed, base_url need to be modified.\"%(username,clustername))\n            oldpublicIP= info['proxy_public_ip']\n            self.update_proxy_ipAndurl(clustername,username,info['proxy_server_ip'])\n            self.update_cluster_baseurl(clustername,username,oldpublicIP,proxy_public_ip)\n            return False\n        else:\n            return True\n\n    def start_cluster(self, clustername, username, user_info):\n        uid = user_info['data']['id']\n        input_rate_limit = user_info['data']['groupinfo']['input_rate_limit']\n        output_rate_limit = user_info['data']['groupinfo']['output_rate_limit']\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if info['status'] == 'running':\n            return [False, \"cluster is already running\"]\n        # set proxy\n        try:\n            target = 'http://'+info['containers'][0]['ip'].split('/')[0]+\":10000\"\n            if self.distributedgw == 'True':\n                worker = self.nodemgr.ip_to_rpc(info['proxy_server_ip'])\n                # check public ip\n                if not self.check_public_ip(clustername,username):\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                worker.set_route(\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername, target)\n            else:\n                if not info['proxy_server_ip'] == self.addr:\n                    logger.info(\"%s %s proxy_server_ip has been changed, base_url need to be modified.\"%(username,clustername))\n                    oldpublicIP= info['proxy_public_ip']\n                    self.update_proxy_ipAndurl(clustername,username,self.addr)\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                    self.update_cluster_baseurl(clustername,username,oldpublicIP,info['proxy_public_ip'])\n                # check public ip\n                if not self.check_public_ip(clustername,username):\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                proxytool.set_route(\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername, target)\n        except:\n            logger.info(traceback.format_exc())\n            return [False, \"start cluster failed with setting proxy failed\"]\n        # check gateway for user\n        # after reboot, user gateway goes down and lose its configuration\n        # so, check is necessary\n        self.networkmgr.check_usergw(input_rate_limit, output_rate_limit, username, uid, self.nodemgr,self.distributedgw=='True')\n        # start containers\n        for container in info['containers']:\n            # set up gre from user's gateway host to container's host.\n            self.networkmgr.check_usergre(username, uid, container['host'], self.nodemgr, self.distributedgw=='True')\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container['host']]\n            worker.start_container(container['containername'])\n            worker.start_services(container['containername'])\n            namesplit = container['containername'].split('-')\n            portname = namesplit[1] + '-' + namesplit[2]\n            worker.recover_usernet(portname, uid, info['proxy_server_ip'], container['host']==info['proxy_server_ip'])\n        [status,vcluster] = self.get_vcluster(clustername,username)\n        vcluster.status ='running'\n        vcluster.start_time = datetime.datetime.now().strftime(\"%Y-%m-%d %H:%M:%S\")\n        vcluster.is_warned = False\n        if not db_commit():\n            return [False, \"Commit Errror\"]\n        return [True, \"start cluster\"]\n\n    def mount_cluster(self, clustername, username):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        for container in info['containers']:\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container['host']]\n            worker.mount_container(container['containername'])\n        return [True, \"mount cluster\"]\n\n    def recover_cluster_on(self, host):\n        logger.info(\"start to recover clusters which has containers on host %s\" % host)\n        vcs = VCluster.query.filter(VCluster.status.in_([\"running\",\"error\"])).all()\n        need_recovers = []\n        for vc in vcs:\n            if len(vc.containers.filter_by(host=host).all()) > 0:\n                need_recovers.append([vc.clustername, vc.ownername])\n        self.recover_clusters(need_recovers)\n    \n    def recover_clusters(self, clusters_users):\n        logger.info(\"start to recover clusters:\"+ str(clusters_users))\n        clusters = [d[0] for d in clusters_users]\n        users = set([d[1] for d in clusters_users])\n        auth_key = env.getenv('AUTH_KEY')\n        res = post_to_user(\"/master/user/groupinfo/\", {'auth_key':auth_key})\n        #logger.info(res)\n        groups = json.loads(res['groups'])\n        quotas = {}\n        for group in groups:\n            #logger.info(group)\n            quotas[group['name']] = group['quotas']\n        recover_data = {}\n        recover_queue = Queue(maxsize=0)\n        for user in users:\n            recover_info = post_to_user(\"/master/user/recoverinfo/\", {'username':user,'auth_key':auth_key})\n            recover_data[user] = {}\n            recover_data[user]['uid'] = recover_info['uid']\n            recover_data[user]['groupname'] = recover_info['groupname']                \n        \n        for d in clusters_users:\n            recover_queue.put({\"user\": d[1], \"vcluster\":d[0], \"times\":0})\n        \n        now_times = 0\n        sleep_time = 10\n        max_times = 1\n        while not recover_queue.empty():\n            q = recover_queue.get()\n            if q['times'] >= max_times:\n                logger.error(\"recovering retry more than %d times, return\" % max_times)\n                return\n            if q['times'] > now_times:\n                logger.error(\"recovering retry %d times, sleep %d seconds\" % (q['times'], sleep_time))\n                time.sleep(sleep_time)\n                sleep_time = 2 * sleep_time\n                now_times = q['times']\n\n            user = q['user']\n            cluster = q['vcluster']\n            logger.info (\"recovering cluster:%s for user:%s %d times...\" % (cluster, user, q['times']))\n            uid = recover_data[user]['uid']\n            groupname = recover_data[user]['groupname']\n            input_rate_limit = quotas[groupname]['input_rate_limit']\n            output_rate_limit = quotas[groupname]['output_rate_limit']\n            success1, msg = self.recover_cluster(cluster, user, uid, input_rate_limit, output_rate_limit)\n            success2, cludb = self.get_vcluster(cluster, user)\n            if not success2:\n                logger.error(\"cannot find vcluster %s\" % cluster)\n                continue\n            if not success1:\n                logger.error(msg)\n                q[\"times\"] += 1\n                recover_queue.put(q)\n                cludb.status = \"error\"\n            else:\n                cludb.status = \"running\"\n            db_commit()\n\n    def recover_cluster(self, clustername, username, uid, input_rate_limit, output_rate_limit):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if info['status'] == 'stopped':\n            return [True, \"cluster no need to start\"]\n        # recover proxy of cluster\n        try:\n            target = 'http://'+info['containers'][0]['ip'].split('/')[0]+\":10000\"\n            if self.distributedgw == 'True':\n                worker = self.nodemgr.ip_to_rpc(info['proxy_server_ip'])\n                # check public ip\n                if not self.check_public_ip(clustername,username):\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                try:\n                    self.nodemgr.call_rpc_function(worker,'set_route',[\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername, target])\n                except Exception as err:\n                    logger.error(traceback.format_exc())\n                    return [False, \"fail to set proxy on node %s\" % info['proxy_server_ip']]\n            else:\n                if not info['proxy_server_ip'] == self.addr:\n                    logger.info(\"%s %s proxy_server_ip has been changed, base_url need to be modified.\"%(username,clustername))\n                    oldpublicIP= info['proxy_public_ip']\n                    self.update_proxy_ipAndurl(clustername,username,self.addr)\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                    self.update_cluster_baseurl(clustername,username,oldpublicIP,info['proxy_public_ip'])\n                # check public ip\n                if not self.check_public_ip(clustername,username):\n                    [status, info] = self.get_clusterinfo(clustername, username)\n                proxytool.set_route(\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername, target)\n        except:\n            return [False, \"start cluster failed with setting proxy failed\"]\n        # need to check and recover gateway of this user\n        self.networkmgr.check_usergw(input_rate_limit, output_rate_limit, username, uid, self.nodemgr,self.distributedgw=='True')\n        # recover containers of this cluster\n        for container in info['containers']:\n            # set up gre from user's gateway host to container's host.\n            self.networkmgr.check_usergre(username, uid, container['host'], self.nodemgr, self.distributedgw=='True')\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container['host']]\n            try:\n                self.nodemgr.call_rpc_function(worker,'recover_container',[container['containername']])\n                namesplit = container['containername'].split('-')\n                portname = namesplit[1] + '-' + namesplit[2]\n                self.nodemgr.call_rpc_function(worker,'recover_usernet',[portname, uid, info['proxy_server_ip'], container['host']==info['proxy_server_ip']])\n            except Exception as err:\n                logger.error(traceback.format_exc())\n                return [False, \"fail to recover container or usernet on node %s\" % container['host']]                \n        # recover ports mapping\n        [success, msg] = self.recover_port_mapping(username,clustername)\n        if not success:\n            return [False, msg]\n        return [True, \"start cluster\"]\n\n    # maybe here should use cluster id\n    def stop_cluster(self, clustername, username):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if info['status'] == 'stopped':\n            return [False, 'cluster is already stopped']\n        if self.distributedgw == 'True':\n            worker = self.nodemgr.ip_to_rpc(info['proxy_server_ip'])\n            worker.delete_route(\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername)\n        else:\n            proxytool.delete_route(\"/\" + info['proxy_public_ip'] + '/go/'+username+'/'+clustername)\n        for container in info['containers']:\n            self.delete_all_port_mapping(username,clustername,container['containername'])\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container['host']]\n            worker.stop_container(container['containername'])\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        vcluster.status = 'stopped'\n        vcluster.start_time =\"------\"\n        vcluster.stop_time = datetime.datetime.now()\n        if not db_commit():\n            return [False, \"Commit Errror\"]\n        return [True, \"stop cluster\"]\n\n    def detach_cluster(self, clustername, username):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if info['status'] == 'running':\n            return [False, 'cluster is running, please stop it first']\n        for container in info['containers']:\n            worker = self.nodemgr.ip_to_rpc(container['host'])\n            if worker is None:\n                return [False, \"The worker %s can't be found or has been stopped.\" % container['host']]\n            worker.detach_container(container['containername'])\n        return [True, \"detach cluster\"]\n\n    def list_clusters(self, user):\n        clusters = VCluster.query.filter_by(ownername = user).all()\n        clusters = [clu.clustername for clu in clusters]\n        '''full_clusters = []\n        for cluster in clusters:\n            single_cluster = {}\n            single_cluster['name'] = cluster\n            [status, info] = self.get_clusterinfo(cluster,user)\n            if info['status'] == 'running':\n                single_cluster['status'] = 'running'\n            else:\n                single_cluster['status'] = 'stopping'\n            full_clusters.append(single_cluster)'''\n        return [True, clusters]\n\n    def migrate_container(self, clustername, username, containername, new_host, user_info):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        if info['status'] != 'stopped':\n            return [False, 'cluster is not stopped']\n\n        con_db = Container.query.get(containername)\n        if con_db is None:\n            return [False, 'Container not found']\n        if con_db.host == new_host:\n            return [False, 'Container has been on the new host']\n\n        oldworker = self.nodemgr.ip_to_rpc(con_db.host)\n        if oldworker is None:\n            return [False, \"Old host worker %s can't be found or has been stopped.\" % con_db.host]\n        oldworker.stop_container(containername)\n        imagename = \"migrate-\" + containername + \"-\" + datetime.datetime.now().strftime(\"%Y-%m-%d\")\n        logger.info(\"Save Image for container:%s imagename:%s host:%s\"%(containername, imagename, con_db.host))\n        status,msg = oldworker.create_image(username,imagename,containername,\"\",10000)\n        if not status:\n            return [False, msg]\n        #con_db.lastsave = datetime.datetime.now()\n        #con_db.image = imagename\n\n        self.networkmgr.load_usrgw(username)\n        proxy_server_ip = self.networkmgr.usrgws[username]\n        [status, proxy_public_ip] = self.etcd.getkey(\"machines/publicIP/\"+proxy_server_ip)\n        if not status:\n            self.imgmgr.removeImage(username,imagename)\n            logger.error(\"Fail to get proxy_public_ip %s.\"%(proxy_server_ip))\n            return [False, \"Fail to get proxy server public IP.\"]\n        uid = user_info['data']['id']\n        setting = {\n                'cpu': con_db.setting_cpu,\n                'memory': con_db.setting_mem,\n                'disk': con_db.setting_disk\n                }\n        _, clusterid, cid = containername.split('-')\n        hostname = \"host-\"+str(cid)\n        gateway = self.networkmgr.get_usergw(username)\n        image = {'name':imagename,'type':'private','owner':username }\n        logger.info(\"Migrate: proxy_ip:%s uid:%s setting:%s clusterid:%s cid:%s hostname:%s gateway:%s image:%s\"\n                    %(proxy_public_ip, str(uid), str(setting), clusterid, cid, hostname, gateway, str(image)))\n        logger.info(\"Migrate: create container(%s) on new host %s\"%(containername, new_host))\n\n        worker = self.nodemgr.ip_to_rpc(new_host)\n        if worker is None:\n            self.imgmgr.removeImage(username,imagename)\n            return [False, \"New host worker %s can't be found or has been stopped.\" % new_host]\n        status,msg = worker.create_container(containername, proxy_public_ip, username, uid, json.dumps(setting),\n                     clustername, str(clusterid), str(cid), hostname, con_db.ip, gateway, json.dumps(image))\n        if not status:\n            self.imgmgr.removeImage(username,imagename)\n            return [False, msg]\n        con_db.host = new_host\n        try:\n            db.session.commit()\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            db.session.rollback()\n            worker.delete_container(containername)\n            self.imgmgr.removeImage(username,imagename)\n            return [False, \"Database commit error!\"]\n        oldworker.delete_container(containername)\n        self.imgmgr.removeImage(username,imagename)\n        return [True,\"\"]\n\n    def migrate_cluster(self, clustername, username, src_host, new_host_list, user_info):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        prestatus = info['status']\n        self.stop_cluster(clustername, username)\n        for container in info['containers']:\n            if not container['host'] == src_host:\n                continue\n            random.shuffle(new_host_list)\n            for new_host in new_host_list:\n                status,msg = self.migrate_container(clustername,username,container['containername'],new_host,user_info)\n                if status:\n                    break\n                else:\n                    logger.error(msg)\n            else:\n                if prestatus == 'running':\n                    self.start_cluster(clustername, username, user_info)\n                return [False, msg]\n        logger.info(\"[Migrate] prestatus:%s for cluster(%s) user(%s)\"%(prestatus, clustername, username))\n        if prestatus == 'running':\n            status, msg = self.start_cluster(clustername, username, user_info)\n            if not status:\n                return [False, msg]\n        return [True, \"\"]\n\n    def migrate_host(self, src_host, new_host_list, ulockmgr):\n        [status, vcluster_list] = self.get_all_clusterinfo()\n        if not status:\n            return [False, vcluster_list]\n        auth_key = env.getenv('AUTH_KEY')\n        res = post_to_user(\"/master/user/groupinfo/\", {'auth_key':auth_key})\n        groups = json.loads(res['groups'])\n        quotas = {}\n        for group in groups:\n            quotas[group['name']] = group['quotas']\n\n        for vcluster in vcluster_list:\n            if 'ownername' not in vcluster.keys():\n                return [Flase, 'Ownername not in vcluster(%s).keys' % str(vcluster) ]\n            try:\n                username = vcluster['ownername']\n                ulockmgr.acquire(username)\n                clustername = vcluster['clustername']\n                rc_info = post_to_user(\"/master/user/recoverinfo/\", {'username':username,'auth_key':auth_key})\n                groupname = rc_info['groupname']\n                user_info = {\"data\":{\"id\":rc_info['uid'],\"groupinfo\":quotas[groupname]}}\n                self.migrate_cluster(clustername, username, src_host, new_host_list, user_info)\n            except Exception as ex:\n                ulockmgr.release(username)\n                logger.error(traceback.format_exc())\n                return [False, str(ex)]\n            ulockmgr.release(username)\n        return [True, \"\"]\n\n    def is_cluster(self, clustername, username):\n        [status, clusters] = self.list_clusters(username)\n        if clustername in clusters:\n            return True\n        else:\n            return False\n\n    # get id from name\n    def get_clusterid(self, clustername, username):\n        [status, info] = self.get_clusterinfo(clustername, username)\n        if not status:\n            return -1\n        if 'clusterid' in info:\n            return int(info['clusterid'])\n        logger.error (\"internal error: cluster:%s info file has no clusterid \" % clustername)\n        return -1\n\n    def update_proxy_ipAndurl(self, clustername, username, proxy_server_ip):\n        [status, vcluster] = self.get_vcluster(clustername, username)\n        if not status:\n            return [False, \"cluster not found\"]\n        vcluster.proxy_server_ip = proxy_server_ip\n        [status, proxy_public_ip] = self.etcd.getkey(\"machines/publicIP/\"+proxy_server_ip)\n        if not status:\n            logger.error(\"Fail to get proxy_public_ip %s.\"%(proxy_server_ip))\n            proxy_public_ip = proxy_server_ip\n        vcluster.proxy_public_ip = proxy_public_ip\n        #proxy_url = env.getenv(\"PORTAL_URL\") +\"/\"+ proxy_public_ip +\"/_web/\" + username + \"/\" + clustername\n        #info['proxy_url'] = proxy_url\n        if not db_commit():\n            return [False, \"Commit Errror\"]\n        return proxy_public_ip\n\n    def get_clusterinfo(self, clustername, username):\n        [success,vcluster] = self.get_vcluster(clustername,username)\n        if vcluster is None:\n            return [False, \"cluster not found\"]\n        vcluster = json.loads(str(vcluster))\n        return [True, vcluster]\n\n    def get_vcluster(self, clustername, username):\n        vcluster = VCluster.query.filter_by(ownername=username,clustername=clustername).first()\n        if vcluster is None:\n            return [False, None]\n        else:\n            return [True, vcluster]\n\n    def get_all_clusterinfo(self):\n        vcluster_list = VCluster.query.all()\n        #logger.info(str(vcluster_list))\n        if vcluster_list is None:\n            return [False, None]\n        else:\n            return [True, json.loads(str(vcluster_list))]\n\n    # acquire cluster id from etcd\n    def _acquire_id(self):\n        self.clusterid_locks.acquire()\n        clusterid = self.etcd.getkey(\"vcluster/nextid\")[1]\n        self.etcd.setkey(\"vcluster/nextid\", str(int(clusterid)+1))\n        self.clusterid_locks.release()\n        return int(clusterid)\n"
  },
  {
    "path": "src/protos/rpc.proto",
    "content": "syntax = \"proto3\";\n\nservice Master {\n\trpc report (ReportMsg) returns (Reply) {}\n}\n\nservice Worker {\n\trpc start_vnode (VNodeInfo) returns (Reply) {}\n\trpc start_task (TaskInfo) returns (Reply) {}\n\trpc stop_task (TaskInfo) returns (Reply) {}\n\trpc stop_vnode (VNodeInfo) returns (Reply) {}\n}\n\nmessage VNodeInfo {\n\tstring taskid = 1;\n\tstring username = 2;\n\tint32 vnodeid = 3;\n\tVNode vnode = 4;\t\t// 集群配置\n}\n\nmessage Reply {\n\tReplyStatus status = 1;\t// 返回值\n\tstring message = 2;\n\n\tenum ReplyStatus {\n\t\tACCEPTED = 0;\n\t\tREFUSED = 1;\n\t}\n}\n\nmessage ReportMsg {\n\trepeated TaskMsg taskmsgs = 1;\n}\n\nmessage TaskMsg {\n\tstring taskid = 1;\n\tstring username = 2;\n\tint32 vnodeid = 3;\n\tStatus subTaskStatus = 4; // 任务状态\n\tstring token = 5;\n\tstring errmsg = 6;\n}\n\nenum Status {\n\tWAITING = 0;\n\tRUNNING = 1;\n\tCOMPLETED = 2;\n\tFAILED = 3;\n\tTIMEOUT = 4;\n\tOUTPUTERROR = 5;\n}\n\nmessage TaskInfo {\n\tstring taskid = 1;\n\tstring username = 2;\n\tint32 vnodeid = 3;\n\tParameters parameters = 4;\t// 参数\n\tint32 timeout = 5;\t\t\t// 超时阈值\n\tstring token = 6;\n}\n\nmessage Parameters {\n\tCommand command = 1;\t\t\t// 命令配置\n\tstring stderrRedirectPath = 2;\t// 错误输出重定向\n\tstring stdoutRedirectPath = 3;\t// 标准输出重定向\n}\n\nmessage Command {\n\tstring commandLine = 1;\t\t\t\t// 命令\n\tstring packagePath = 2;\t\t\t\t// 工作路径\n\tmap<string, string> envVars = 3;\t// 自定义环境变量\n}\n\nmessage VNode {\n\tImage image = 1;\t\t\t// 镜像配置\n\tInstance instance = 2;\t\t// 实例配置\n\trepeated Mount mount = 3;\t// 挂载配置\n  Network network = 4; //网络配置\n\tstring hostname = 5; //主机名\n}\n\nmessage Network {\n\tstring ipaddr = 1;\n\tstring gateway = 2;\n\tstring masterip = 3;\n\tstring brname = 4;\n}\n\nmessage Image {\n\tstring name = 1;\t// 镜像名\n\tImageType type = 2;\t// 镜像类型（public/private)\n\tstring owner = 3;\t// 所有者\n\n\tenum ImageType {\n\tBASE = 0;\n\tPUBLIC = 1;\n\tPRIVATE = 2;\n\t}\n}\n\nmessage Mount {\n\tstring provider = 1;\n\tstring localPath = 2;\t// 本地路径\n\tstring remotePath = 3;\t// 远程路径\n\tstring accessKey = 4;\n\tstring secretKey = 5;\n\tstring other = 6;\n}\n\nmessage Instance {\n\tint32 cpu = 1;\t\t// CPU，单位 个？\n\tint32 memory = 2;\t// 内存，单位 mb\n\tint32 disk = 3;\t\t// 磁盘，单位 mb\n\tint32 gpu = 4;\t\t// 显卡，单位 个\n}\n"
  },
  {
    "path": "src/protos/rpc_pb2.py",
    "content": "# Generated by the protocol buffer compiler.  DO NOT EDIT!\n# source: rpc.proto\n\nimport sys\n_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))\nfrom google.protobuf.internal import enum_type_wrapper\nfrom google.protobuf import descriptor as _descriptor\nfrom google.protobuf import message as _message\nfrom google.protobuf import reflection as _reflection\nfrom google.protobuf import symbol_database as _symbol_database\nfrom google.protobuf import descriptor_pb2\n# @@protoc_insertion_point(imports)\n\n_sym_db = _symbol_database.Default()\n\n\n\n\nDESCRIPTOR = _descriptor.FileDescriptor(\n  name='rpc.proto',\n  package='',\n  syntax='proto3',\n  serialized_pb=_b('\\n\\trpc.proto\\\"U\\n\\tVNodeInfo\\x12\\x0e\\n\\x06taskid\\x18\\x01 \\x01(\\t\\x12\\x10\\n\\x08username\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07vnodeid\\x18\\x03 \\x01(\\x05\\x12\\x15\\n\\x05vnode\\x18\\x04 \\x01(\\x0b\\x32\\x06.VNode\\\"f\\n\\x05Reply\\x12\\\"\\n\\x06status\\x18\\x01 \\x01(\\x0e\\x32\\x12.Reply.ReplyStatus\\x12\\x0f\\n\\x07message\\x18\\x02 \\x01(\\t\\\"(\\n\\x0bReplyStatus\\x12\\x0c\\n\\x08\\x41\\x43\\x43\\x45PTED\\x10\\x00\\x12\\x0b\\n\\x07REFUSED\\x10\\x01\\\"\\'\\n\\tReportMsg\\x12\\x1a\\n\\x08taskmsgs\\x18\\x01 \\x03(\\x0b\\x32\\x08.TaskMsg\\\"{\\n\\x07TaskMsg\\x12\\x0e\\n\\x06taskid\\x18\\x01 \\x01(\\t\\x12\\x10\\n\\x08username\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07vnodeid\\x18\\x03 \\x01(\\x05\\x12\\x1e\\n\\rsubTaskStatus\\x18\\x04 \\x01(\\x0e\\x32\\x07.Status\\x12\\r\\n\\x05token\\x18\\x05 \\x01(\\t\\x12\\x0e\\n\\x06\\x65rrmsg\\x18\\x06 \\x01(\\t\\\"~\\n\\x08TaskInfo\\x12\\x0e\\n\\x06taskid\\x18\\x01 \\x01(\\t\\x12\\x10\\n\\x08username\\x18\\x02 \\x01(\\t\\x12\\x0f\\n\\x07vnodeid\\x18\\x03 \\x01(\\x05\\x12\\x1f\\n\\nparameters\\x18\\x04 \\x01(\\x0b\\x32\\x0b.Parameters\\x12\\x0f\\n\\x07timeout\\x18\\x05 \\x01(\\x05\\x12\\r\\n\\x05token\\x18\\x06 \\x01(\\t\\\"_\\n\\nParameters\\x12\\x19\\n\\x07\\x63ommand\\x18\\x01 \\x01(\\x0b\\x32\\x08.Command\\x12\\x1a\\n\\x12stderrRedirectPath\\x18\\x02 \\x01(\\t\\x12\\x1a\\n\\x12stdoutRedirectPath\\x18\\x03 \\x01(\\t\\\"\\x8b\\x01\\n\\x07\\x43ommand\\x12\\x13\\n\\x0b\\x63ommandLine\\x18\\x01 \\x01(\\t\\x12\\x13\\n\\x0bpackagePath\\x18\\x02 \\x01(\\t\\x12&\\n\\x07\\x65nvVars\\x18\\x03 \\x03(\\x0b\\x32\\x15.Command.EnvVarsEntry\\x1a.\\n\\x0c\\x45nvVarsEntry\\x12\\x0b\\n\\x03key\\x18\\x01 \\x01(\\t\\x12\\r\\n\\x05value\\x18\\x02 \\x01(\\t:\\x02\\x38\\x01\\\"\\x7f\\n\\x05VNode\\x12\\x15\\n\\x05image\\x18\\x01 \\x01(\\x0b\\x32\\x06.Image\\x12\\x1b\\n\\x08instance\\x18\\x02 \\x01(\\x0b\\x32\\t.Instance\\x12\\x15\\n\\x05mount\\x18\\x03 \\x03(\\x0b\\x32\\x06.Mount\\x12\\x19\\n\\x07network\\x18\\x04 \\x01(\\x0b\\x32\\x08.Network\\x12\\x10\\n\\x08hostname\\x18\\x05 \\x01(\\t\\\"L\\n\\x07Network\\x12\\x0e\\n\\x06ipaddr\\x18\\x01 \\x01(\\t\\x12\\x0f\\n\\x07gateway\\x18\\x02 \\x01(\\t\\x12\\x10\\n\\x08masterip\\x18\\x03 \\x01(\\t\\x12\\x0e\\n\\x06\\x62rname\\x18\\x04 \\x01(\\t\\\"t\\n\\x05Image\\x12\\x0c\\n\\x04name\\x18\\x01 \\x01(\\t\\x12\\x1e\\n\\x04type\\x18\\x02 \\x01(\\x0e\\x32\\x10.Image.ImageType\\x12\\r\\n\\x05owner\\x18\\x03 \\x01(\\t\\\".\\n\\tImageType\\x12\\x08\\n\\x04\\x42\\x41SE\\x10\\x00\\x12\\n\\n\\x06PUBLIC\\x10\\x01\\x12\\x0b\\n\\x07PRIVATE\\x10\\x02\\\"u\\n\\x05Mount\\x12\\x10\\n\\x08provider\\x18\\x01 \\x01(\\t\\x12\\x11\\n\\tlocalPath\\x18\\x02 \\x01(\\t\\x12\\x12\\n\\nremotePath\\x18\\x03 \\x01(\\t\\x12\\x11\\n\\taccessKey\\x18\\x04 \\x01(\\t\\x12\\x11\\n\\tsecretKey\\x18\\x05 \\x01(\\t\\x12\\r\\n\\x05other\\x18\\x06 \\x01(\\t\\\"B\\n\\x08Instance\\x12\\x0b\\n\\x03\\x63pu\\x18\\x01 \\x01(\\x05\\x12\\x0e\\n\\x06memory\\x18\\x02 \\x01(\\x05\\x12\\x0c\\n\\x04\\x64isk\\x18\\x03 \\x01(\\x05\\x12\\x0b\\n\\x03gpu\\x18\\x04 \\x01(\\x05*[\\n\\x06Status\\x12\\x0b\\n\\x07WAITING\\x10\\x00\\x12\\x0b\\n\\x07RUNNING\\x10\\x01\\x12\\r\\n\\tCOMPLETED\\x10\\x02\\x12\\n\\n\\x06\\x46\\x41ILED\\x10\\x03\\x12\\x0b\\n\\x07TIMEOUT\\x10\\x04\\x12\\x0f\\n\\x0bOUTPUTERROR\\x10\\x05\\x32(\\n\\x06Master\\x12\\x1e\\n\\x06report\\x12\\n.ReportMsg\\x1a\\x06.Reply\\\"\\x00\\x32\\x96\\x01\\n\\x06Worker\\x12#\\n\\x0bstart_vnode\\x12\\n.VNodeInfo\\x1a\\x06.Reply\\\"\\x00\\x12!\\n\\nstart_task\\x12\\t.TaskInfo\\x1a\\x06.Reply\\\"\\x00\\x12 \\n\\tstop_task\\x12\\t.TaskInfo\\x1a\\x06.Reply\\\"\\x00\\x12\\\"\\n\\nstop_vnode\\x12\\n.VNodeInfo\\x1a\\x06.Reply\\\"\\x00\\x62\\x06proto3')\n)\n\n_STATUS = _descriptor.EnumDescriptor(\n  name='Status',\n  full_name='Status',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='WAITING', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='RUNNING', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='COMPLETED', index=2, number=2,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='FAILED', index=3, number=3,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='TIMEOUT', index=4, number=4,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='OUTPUTERROR', index=5, number=5,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=1249,\n  serialized_end=1340,\n)\n_sym_db.RegisterEnumDescriptor(_STATUS)\n\nStatus = enum_type_wrapper.EnumTypeWrapper(_STATUS)\nWAITING = 0\nRUNNING = 1\nCOMPLETED = 2\nFAILED = 3\nTIMEOUT = 4\nOUTPUTERROR = 5\n\n\n_REPLY_REPLYSTATUS = _descriptor.EnumDescriptor(\n  name='ReplyStatus',\n  full_name='Reply.ReplyStatus',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='ACCEPTED', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='REFUSED', index=1, number=1,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=162,\n  serialized_end=202,\n)\n_sym_db.RegisterEnumDescriptor(_REPLY_REPLYSTATUS)\n\n_IMAGE_IMAGETYPE = _descriptor.EnumDescriptor(\n  name='ImageType',\n  full_name='Image.ImageType',\n  filename=None,\n  file=DESCRIPTOR,\n  values=[\n    _descriptor.EnumValueDescriptor(\n      name='BASE', index=0, number=0,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PUBLIC', index=1, number=1,\n      options=None,\n      type=None),\n    _descriptor.EnumValueDescriptor(\n      name='PRIVATE', index=2, number=2,\n      options=None,\n      type=None),\n  ],\n  containing_type=None,\n  options=None,\n  serialized_start=1014,\n  serialized_end=1060,\n)\n_sym_db.RegisterEnumDescriptor(_IMAGE_IMAGETYPE)\n\n\n_VNODEINFO = _descriptor.Descriptor(\n  name='VNodeInfo',\n  full_name='VNodeInfo',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='taskid', full_name='VNodeInfo.taskid', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='username', full_name='VNodeInfo.username', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='vnodeid', full_name='VNodeInfo.vnodeid', index=2,\n      number=3, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='vnode', full_name='VNodeInfo.vnode', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=13,\n  serialized_end=98,\n)\n\n\n_REPLY = _descriptor.Descriptor(\n  name='Reply',\n  full_name='Reply',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='status', full_name='Reply.status', index=0,\n      number=1, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='message', full_name='Reply.message', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _REPLY_REPLYSTATUS,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=100,\n  serialized_end=202,\n)\n\n\n_REPORTMSG = _descriptor.Descriptor(\n  name='ReportMsg',\n  full_name='ReportMsg',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='taskmsgs', full_name='ReportMsg.taskmsgs', index=0,\n      number=1, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=204,\n  serialized_end=243,\n)\n\n\n_TASKMSG = _descriptor.Descriptor(\n  name='TaskMsg',\n  full_name='TaskMsg',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='taskid', full_name='TaskMsg.taskid', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='username', full_name='TaskMsg.username', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='vnodeid', full_name='TaskMsg.vnodeid', index=2,\n      number=3, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='subTaskStatus', full_name='TaskMsg.subTaskStatus', index=3,\n      number=4, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='token', full_name='TaskMsg.token', index=4,\n      number=5, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='errmsg', full_name='TaskMsg.errmsg', index=5,\n      number=6, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=245,\n  serialized_end=368,\n)\n\n\n_TASKINFO = _descriptor.Descriptor(\n  name='TaskInfo',\n  full_name='TaskInfo',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='taskid', full_name='TaskInfo.taskid', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='username', full_name='TaskInfo.username', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='vnodeid', full_name='TaskInfo.vnodeid', index=2,\n      number=3, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='parameters', full_name='TaskInfo.parameters', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='timeout', full_name='TaskInfo.timeout', index=4,\n      number=5, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='token', full_name='TaskInfo.token', index=5,\n      number=6, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=370,\n  serialized_end=496,\n)\n\n\n_PARAMETERS = _descriptor.Descriptor(\n  name='Parameters',\n  full_name='Parameters',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='command', full_name='Parameters.command', index=0,\n      number=1, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='stderrRedirectPath', full_name='Parameters.stderrRedirectPath', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='stdoutRedirectPath', full_name='Parameters.stdoutRedirectPath', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=498,\n  serialized_end=593,\n)\n\n\n_COMMAND_ENVVARSENTRY = _descriptor.Descriptor(\n  name='EnvVarsEntry',\n  full_name='Command.EnvVarsEntry',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='key', full_name='Command.EnvVarsEntry.key', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='value', full_name='Command.EnvVarsEntry.value', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=_descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\\001')),\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=689,\n  serialized_end=735,\n)\n\n_COMMAND = _descriptor.Descriptor(\n  name='Command',\n  full_name='Command',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='commandLine', full_name='Command.commandLine', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='packagePath', full_name='Command.packagePath', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='envVars', full_name='Command.envVars', index=2,\n      number=3, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[_COMMAND_ENVVARSENTRY, ],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=596,\n  serialized_end=735,\n)\n\n\n_VNODE = _descriptor.Descriptor(\n  name='VNode',\n  full_name='VNode',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='image', full_name='VNode.image', index=0,\n      number=1, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='instance', full_name='VNode.instance', index=1,\n      number=2, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='mount', full_name='VNode.mount', index=2,\n      number=3, type=11, cpp_type=10, label=3,\n      has_default_value=False, default_value=[],\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='network', full_name='VNode.network', index=3,\n      number=4, type=11, cpp_type=10, label=1,\n      has_default_value=False, default_value=None,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='hostname', full_name='VNode.hostname', index=4,\n      number=5, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=737,\n  serialized_end=864,\n)\n\n\n_NETWORK = _descriptor.Descriptor(\n  name='Network',\n  full_name='Network',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='ipaddr', full_name='Network.ipaddr', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='gateway', full_name='Network.gateway', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='masterip', full_name='Network.masterip', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='brname', full_name='Network.brname', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=866,\n  serialized_end=942,\n)\n\n\n_IMAGE = _descriptor.Descriptor(\n  name='Image',\n  full_name='Image',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='name', full_name='Image.name', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='type', full_name='Image.type', index=1,\n      number=2, type=14, cpp_type=8, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='owner', full_name='Image.owner', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n    _IMAGE_IMAGETYPE,\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=944,\n  serialized_end=1060,\n)\n\n\n_MOUNT = _descriptor.Descriptor(\n  name='Mount',\n  full_name='Mount',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='provider', full_name='Mount.provider', index=0,\n      number=1, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='localPath', full_name='Mount.localPath', index=1,\n      number=2, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='remotePath', full_name='Mount.remotePath', index=2,\n      number=3, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='accessKey', full_name='Mount.accessKey', index=3,\n      number=4, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='secretKey', full_name='Mount.secretKey', index=4,\n      number=5, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='other', full_name='Mount.other', index=5,\n      number=6, type=9, cpp_type=9, label=1,\n      has_default_value=False, default_value=_b(\"\").decode('utf-8'),\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1062,\n  serialized_end=1179,\n)\n\n\n_INSTANCE = _descriptor.Descriptor(\n  name='Instance',\n  full_name='Instance',\n  filename=None,\n  file=DESCRIPTOR,\n  containing_type=None,\n  fields=[\n    _descriptor.FieldDescriptor(\n      name='cpu', full_name='Instance.cpu', index=0,\n      number=1, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='memory', full_name='Instance.memory', index=1,\n      number=2, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='disk', full_name='Instance.disk', index=2,\n      number=3, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n    _descriptor.FieldDescriptor(\n      name='gpu', full_name='Instance.gpu', index=3,\n      number=4, type=5, cpp_type=1, label=1,\n      has_default_value=False, default_value=0,\n      message_type=None, enum_type=None, containing_type=None,\n      is_extension=False, extension_scope=None,\n      options=None, file=DESCRIPTOR),\n  ],\n  extensions=[\n  ],\n  nested_types=[],\n  enum_types=[\n  ],\n  options=None,\n  is_extendable=False,\n  syntax='proto3',\n  extension_ranges=[],\n  oneofs=[\n  ],\n  serialized_start=1181,\n  serialized_end=1247,\n)\n\n_VNODEINFO.fields_by_name['vnode'].message_type = _VNODE\n_REPLY.fields_by_name['status'].enum_type = _REPLY_REPLYSTATUS\n_REPLY_REPLYSTATUS.containing_type = _REPLY\n_REPORTMSG.fields_by_name['taskmsgs'].message_type = _TASKMSG\n_TASKMSG.fields_by_name['subTaskStatus'].enum_type = _STATUS\n_TASKINFO.fields_by_name['parameters'].message_type = _PARAMETERS\n_PARAMETERS.fields_by_name['command'].message_type = _COMMAND\n_COMMAND_ENVVARSENTRY.containing_type = _COMMAND\n_COMMAND.fields_by_name['envVars'].message_type = _COMMAND_ENVVARSENTRY\n_VNODE.fields_by_name['image'].message_type = _IMAGE\n_VNODE.fields_by_name['instance'].message_type = _INSTANCE\n_VNODE.fields_by_name['mount'].message_type = _MOUNT\n_VNODE.fields_by_name['network'].message_type = _NETWORK\n_IMAGE.fields_by_name['type'].enum_type = _IMAGE_IMAGETYPE\n_IMAGE_IMAGETYPE.containing_type = _IMAGE\nDESCRIPTOR.message_types_by_name['VNodeInfo'] = _VNODEINFO\nDESCRIPTOR.message_types_by_name['Reply'] = _REPLY\nDESCRIPTOR.message_types_by_name['ReportMsg'] = _REPORTMSG\nDESCRIPTOR.message_types_by_name['TaskMsg'] = _TASKMSG\nDESCRIPTOR.message_types_by_name['TaskInfo'] = _TASKINFO\nDESCRIPTOR.message_types_by_name['Parameters'] = _PARAMETERS\nDESCRIPTOR.message_types_by_name['Command'] = _COMMAND\nDESCRIPTOR.message_types_by_name['VNode'] = _VNODE\nDESCRIPTOR.message_types_by_name['Network'] = _NETWORK\nDESCRIPTOR.message_types_by_name['Image'] = _IMAGE\nDESCRIPTOR.message_types_by_name['Mount'] = _MOUNT\nDESCRIPTOR.message_types_by_name['Instance'] = _INSTANCE\nDESCRIPTOR.enum_types_by_name['Status'] = _STATUS\n_sym_db.RegisterFileDescriptor(DESCRIPTOR)\n\nVNodeInfo = _reflection.GeneratedProtocolMessageType('VNodeInfo', (_message.Message,), dict(\n  DESCRIPTOR = _VNODEINFO,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:VNodeInfo)\n  ))\n_sym_db.RegisterMessage(VNodeInfo)\n\nReply = _reflection.GeneratedProtocolMessageType('Reply', (_message.Message,), dict(\n  DESCRIPTOR = _REPLY,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Reply)\n  ))\n_sym_db.RegisterMessage(Reply)\n\nReportMsg = _reflection.GeneratedProtocolMessageType('ReportMsg', (_message.Message,), dict(\n  DESCRIPTOR = _REPORTMSG,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:ReportMsg)\n  ))\n_sym_db.RegisterMessage(ReportMsg)\n\nTaskMsg = _reflection.GeneratedProtocolMessageType('TaskMsg', (_message.Message,), dict(\n  DESCRIPTOR = _TASKMSG,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:TaskMsg)\n  ))\n_sym_db.RegisterMessage(TaskMsg)\n\nTaskInfo = _reflection.GeneratedProtocolMessageType('TaskInfo', (_message.Message,), dict(\n  DESCRIPTOR = _TASKINFO,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:TaskInfo)\n  ))\n_sym_db.RegisterMessage(TaskInfo)\n\nParameters = _reflection.GeneratedProtocolMessageType('Parameters', (_message.Message,), dict(\n  DESCRIPTOR = _PARAMETERS,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Parameters)\n  ))\n_sym_db.RegisterMessage(Parameters)\n\nCommand = _reflection.GeneratedProtocolMessageType('Command', (_message.Message,), dict(\n\n  EnvVarsEntry = _reflection.GeneratedProtocolMessageType('EnvVarsEntry', (_message.Message,), dict(\n    DESCRIPTOR = _COMMAND_ENVVARSENTRY,\n    __module__ = 'rpc_pb2'\n    # @@protoc_insertion_point(class_scope:Command.EnvVarsEntry)\n    ))\n  ,\n  DESCRIPTOR = _COMMAND,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Command)\n  ))\n_sym_db.RegisterMessage(Command)\n_sym_db.RegisterMessage(Command.EnvVarsEntry)\n\nVNode = _reflection.GeneratedProtocolMessageType('VNode', (_message.Message,), dict(\n  DESCRIPTOR = _VNODE,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:VNode)\n  ))\n_sym_db.RegisterMessage(VNode)\n\nNetwork = _reflection.GeneratedProtocolMessageType('Network', (_message.Message,), dict(\n  DESCRIPTOR = _NETWORK,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Network)\n  ))\n_sym_db.RegisterMessage(Network)\n\nImage = _reflection.GeneratedProtocolMessageType('Image', (_message.Message,), dict(\n  DESCRIPTOR = _IMAGE,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Image)\n  ))\n_sym_db.RegisterMessage(Image)\n\nMount = _reflection.GeneratedProtocolMessageType('Mount', (_message.Message,), dict(\n  DESCRIPTOR = _MOUNT,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Mount)\n  ))\n_sym_db.RegisterMessage(Mount)\n\nInstance = _reflection.GeneratedProtocolMessageType('Instance', (_message.Message,), dict(\n  DESCRIPTOR = _INSTANCE,\n  __module__ = 'rpc_pb2'\n  # @@protoc_insertion_point(class_scope:Instance)\n  ))\n_sym_db.RegisterMessage(Instance)\n\n\n_COMMAND_ENVVARSENTRY.has_options = True\n_COMMAND_ENVVARSENTRY._options = _descriptor._ParseOptions(descriptor_pb2.MessageOptions(), _b('8\\001'))\n\n_MASTER = _descriptor.ServiceDescriptor(\n  name='Master',\n  full_name='Master',\n  file=DESCRIPTOR,\n  index=0,\n  options=None,\n  serialized_start=1342,\n  serialized_end=1382,\n  methods=[\n  _descriptor.MethodDescriptor(\n    name='report',\n    full_name='Master.report',\n    index=0,\n    containing_service=None,\n    input_type=_REPORTMSG,\n    output_type=_REPLY,\n    options=None,\n  ),\n])\n_sym_db.RegisterServiceDescriptor(_MASTER)\n\nDESCRIPTOR.services_by_name['Master'] = _MASTER\n\n\n_WORKER = _descriptor.ServiceDescriptor(\n  name='Worker',\n  full_name='Worker',\n  file=DESCRIPTOR,\n  index=1,\n  options=None,\n  serialized_start=1385,\n  serialized_end=1535,\n  methods=[\n  _descriptor.MethodDescriptor(\n    name='start_vnode',\n    full_name='Worker.start_vnode',\n    index=0,\n    containing_service=None,\n    input_type=_VNODEINFO,\n    output_type=_REPLY,\n    options=None,\n  ),\n  _descriptor.MethodDescriptor(\n    name='start_task',\n    full_name='Worker.start_task',\n    index=1,\n    containing_service=None,\n    input_type=_TASKINFO,\n    output_type=_REPLY,\n    options=None,\n  ),\n  _descriptor.MethodDescriptor(\n    name='stop_task',\n    full_name='Worker.stop_task',\n    index=2,\n    containing_service=None,\n    input_type=_TASKINFO,\n    output_type=_REPLY,\n    options=None,\n  ),\n  _descriptor.MethodDescriptor(\n    name='stop_vnode',\n    full_name='Worker.stop_vnode',\n    index=3,\n    containing_service=None,\n    input_type=_VNODEINFO,\n    output_type=_REPLY,\n    options=None,\n  ),\n])\n_sym_db.RegisterServiceDescriptor(_WORKER)\n\nDESCRIPTOR.services_by_name['Worker'] = _WORKER\n\n# @@protoc_insertion_point(module_scope)\n"
  },
  {
    "path": "src/protos/rpc_pb2_grpc.py",
    "content": "# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!\nimport grpc\n\nfrom protos import rpc_pb2 as rpc__pb2\n\n\nclass MasterStub(object):\n  # missing associated documentation comment in .proto file\n  pass\n\n  def __init__(self, channel):\n    \"\"\"Constructor.\n\n    Args:\n      channel: A grpc.Channel.\n    \"\"\"\n    self.report = channel.unary_unary(\n        '/Master/report',\n        request_serializer=rpc__pb2.ReportMsg.SerializeToString,\n        response_deserializer=rpc__pb2.Reply.FromString,\n        )\n\n\nclass MasterServicer(object):\n  # missing associated documentation comment in .proto file\n  pass\n\n  def report(self, request, context):\n    # missing associated documentation comment in .proto file\n    pass\n    context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n    context.set_details('Method not implemented!')\n    raise NotImplementedError('Method not implemented!')\n\n\ndef add_MasterServicer_to_server(servicer, server):\n  rpc_method_handlers = {\n      'report': grpc.unary_unary_rpc_method_handler(\n          servicer.report,\n          request_deserializer=rpc__pb2.ReportMsg.FromString,\n          response_serializer=rpc__pb2.Reply.SerializeToString,\n      ),\n  }\n  generic_handler = grpc.method_handlers_generic_handler(\n      'Master', rpc_method_handlers)\n  server.add_generic_rpc_handlers((generic_handler,))\n\n\nclass WorkerStub(object):\n  # missing associated documentation comment in .proto file\n  pass\n\n  def __init__(self, channel):\n    \"\"\"Constructor.\n\n    Args:\n      channel: A grpc.Channel.\n    \"\"\"\n    self.start_vnode = channel.unary_unary(\n        '/Worker/start_vnode',\n        request_serializer=rpc__pb2.VNodeInfo.SerializeToString,\n        response_deserializer=rpc__pb2.Reply.FromString,\n        )\n    self.start_task = channel.unary_unary(\n        '/Worker/start_task',\n        request_serializer=rpc__pb2.TaskInfo.SerializeToString,\n        response_deserializer=rpc__pb2.Reply.FromString,\n        )\n    self.stop_task = channel.unary_unary(\n        '/Worker/stop_task',\n        request_serializer=rpc__pb2.TaskInfo.SerializeToString,\n        response_deserializer=rpc__pb2.Reply.FromString,\n        )\n    self.stop_vnode = channel.unary_unary(\n        '/Worker/stop_vnode',\n        request_serializer=rpc__pb2.VNodeInfo.SerializeToString,\n        response_deserializer=rpc__pb2.Reply.FromString,\n        )\n\n\nclass WorkerServicer(object):\n  # missing associated documentation comment in .proto file\n  pass\n\n  def start_vnode(self, request, context):\n    # missing associated documentation comment in .proto file\n    pass\n    context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n    context.set_details('Method not implemented!')\n    raise NotImplementedError('Method not implemented!')\n\n  def start_task(self, request, context):\n    # missing associated documentation comment in .proto file\n    pass\n    context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n    context.set_details('Method not implemented!')\n    raise NotImplementedError('Method not implemented!')\n\n  def stop_task(self, request, context):\n    # missing associated documentation comment in .proto file\n    pass\n    context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n    context.set_details('Method not implemented!')\n    raise NotImplementedError('Method not implemented!')\n\n  def stop_vnode(self, request, context):\n    # missing associated documentation comment in .proto file\n    pass\n    context.set_code(grpc.StatusCode.UNIMPLEMENTED)\n    context.set_details('Method not implemented!')\n    raise NotImplementedError('Method not implemented!')\n\n\ndef add_WorkerServicer_to_server(servicer, server):\n  rpc_method_handlers = {\n      'start_vnode': grpc.unary_unary_rpc_method_handler(\n          servicer.start_vnode,\n          request_deserializer=rpc__pb2.VNodeInfo.FromString,\n          response_serializer=rpc__pb2.Reply.SerializeToString,\n      ),\n      'start_task': grpc.unary_unary_rpc_method_handler(\n          servicer.start_task,\n          request_deserializer=rpc__pb2.TaskInfo.FromString,\n          response_serializer=rpc__pb2.Reply.SerializeToString,\n      ),\n      'stop_task': grpc.unary_unary_rpc_method_handler(\n          servicer.stop_task,\n          request_deserializer=rpc__pb2.TaskInfo.FromString,\n          response_serializer=rpc__pb2.Reply.SerializeToString,\n      ),\n      'stop_vnode': grpc.unary_unary_rpc_method_handler(\n          servicer.stop_vnode,\n          request_deserializer=rpc__pb2.VNodeInfo.FromString,\n          response_serializer=rpc__pb2.Reply.SerializeToString,\n      ),\n  }\n  generic_handler = grpc.method_handlers_generic_handler(\n      'Worker', rpc_method_handlers)\n  server.add_generic_rpc_handlers((generic_handler,))\n"
  },
  {
    "path": "src/utils/env.py",
    "content": "import os,netifaces\n\ndef getenv(key):\n    if key == \"CLUSTER_NAME\":\n        return os.environ.get(\"CLUSTER_NAME\", \"docklet-vc\")\n    elif key == \"FS_PREFIX\":\n        return os.environ.get(\"FS_PREFIX\", \"/opt/docklet\")\n    elif key == \"CLUSTER_SIZE\":\n        return int(os.environ.get(\"CLUSTER_SIZE\", 1))\n    elif key == \"CLUSTER_NET\":\n        return os.environ.get(\"CLUSTER_NET\", \"172.16.0.1/16\")\n    elif key == \"CONTAINER_CPU\":\n        return int(os.environ.get(\"CONTAINER_CPU\", 100000))\n    elif key == \"CONTAINER_DISK\":\n        return int(os.environ.get(\"CONTAINER_DISK\", 1000))\n    elif key == \"CONTAINER_MEMORY\":\n        return int(os.environ.get(\"CONTAINER_MEMORY\", 1000))\n    elif key == \"DISKPOOL_SIZE\":\n        return int(os.environ.get(\"DISKPOOL_SIZE\", 10000))\n    elif key == \"ETCD\":\n        return os.environ.get(\"ETCD\", \"localhost:2379\")\n    elif key == \"NETWORK_DEVICE\":\n        return os.environ.get(\"NETWORK_DEVICE\", \"eth0\")\n    elif key == \"MASTER_IP\":\n        return os.environ.get(\"MASTER_IP\", \"0.0.0.0\")\n    elif key == \"MASTER_IPS\":\n        return os.environ.get(\"MASTER_IPS\", \"0.0.0.0@docklet\")\n    elif key == \"MASTER_PORT\":\n        return int(os.environ.get(\"MASTER_PORT\", 9000))\n    elif key == \"WORKER_PORT\":\n        return int(os.environ.get(\"WORKER_PORT\", 9001))\n    elif key == \"NGINX_PORT\":\n        return int(os.environ.get(\"NGINX_PORT\", 8080))\n    elif key == \"PROXY_PORT\":\n        return int(os.environ.get(\"PROXY_PORT\", 8000))\n    elif key == \"PROXY_API_PORT\":\n        return int(os.environ.get(\"PROXY_API_PORT\", 8001))\n    elif key == \"WEB_PORT\":\n        return int(os.environ.get(\"WEB_PORT\", 8888))\n    elif key == \"PORTAL_URL\":\n        return os.environ.get(\"PORTAL_URL\",\n            \"http://\"+getenv(\"MASTER_IP\") + \":\" + str(getenv(\"NGINX_PORT\")))\n    elif key == \"LOG_LEVEL\":\n        return os.environ.get(\"LOG_LEVEL\", \"DEBUG\")\n    elif key == \"LOG_LIFE\":\n        return int(os.environ.get(\"LOG_LIFE\", 10))\n    elif key == \"WEB_LOG_LEVEL\":\n        return os.environ.get(\"WEB_LOG_LEVEL\", \"DEBUG\")\n    elif key == \"STORAGE\":\n        return os.environ.get(\"STORAGE\", \"file\")\n    elif key ==\"EXTERNAL_LOGIN\":\n        return os.environ.get(\"EXTERNAL_LOGIN\", \"False\")\n    elif key ==\"DATA_QUOTA\":\n        return os.environ.get(\"DATA_QUOTA\", \"False\")\n    elif key ==\"DATA_QUOTA_CMD\":\n        return os.environ.get(\"DATA_QUOTA_CMD\", \"gluster volume quota docklet-volume limit-usage %s %s\")\n    elif key == 'DISTRIBUTED_GATEWAY':\n        return os.environ.get(\"DISTRIBUTED_GATEWAY\", \"False\")\n    elif key == \"PUBLIC_IP\":\n        device = os.environ.get(\"NETWORK_DEVICE\",\"eth0\")\n        addr = netifaces.ifaddresses(device)\n        if 2 in addr:\n            return os.environ.get(\"PUBLIC_IP\",addr[2][0]['addr'])\n        else:\n            return os.environ.get(\"PUBLIC_IP\",\"0.0.0.0\")\n    elif key == \"NGINX_CONF\":\n        return os.environ.get(\"NGINX_CONF\",\"/etc/nginx\")\n    elif key ==\"USER_IP\":\n        return os.environ.get(\"USER_IP\",\"0.0.0.0\")\n    elif key ==\"USER_PORT\":\n        return int(os.environ.get(\"USER_PORT\",9100))\n    elif key ==\"AUTH_KEY\":\n        return os.environ.get(\"AUTH_KEY\",\"docklet\")\n    elif key ==\"OPEN_REGISTRY\":\n        return os.environ.get(\"OPEN_REGISTRY\",\"False\")\n    elif key ==\"APPROVAL_RBT\":\n        return os.environ.get(\"APPROVAL_RBT\",\"ON\")\n    elif key ==\"ALLOCATED_PORTS\":\n        return os.environ.get(\"ALLOCATED_PORTS\",\"10000-65535\")\n    elif key ==\"ALLOW_SCALE_OUT\":\n        return os.environ.get(\"ALLOW_SCALE_OUT\", \"False\")\n    elif key == \"WARNING_DAYS\":\n        return os.environ.get(\"WARNING_DAYS\", \"7\")\n    elif key == \"RELEASE_DAYS\":\n        return os.environ.get(\"RELEASE_DAYS\", \"14\")\n    elif key == \"BATCH_ON\":\n        return os.environ.get(\"BATCH_ON\",\"True\")\n    elif key == \"BATCH_MASTER_PORT\":\n        return os.environ.get(\"BATCH_MASTER_PORT\",\"50050\")\n    elif key == \"BATCH_WORKER_PORT\":\n        return os.environ.get(\"BATCH_WORKER_PORT\",\"50051\")\n    elif key == \"BATCH_TASK_CIDR\":\n        return os.environ.get(\"BATCH_TASK_CIDR\",\"4\")\n    elif key == \"BATCH_NET\":\n        return os.environ.get(\"BATCH_NET\",\"10.16.0.0/16\")\n    elif key == \"BATCH_MAX_THREAD_WORKER\":\n        return os.environ.get(\"BATCH_MAX_THREAD_WORKER\",\"5\")\n    else:\n        return os.environ.get(key,\"\")\n"
  },
  {
    "path": "src/utils/etcdlib.py",
    "content": "#!/usr/bin/python3\n\n############################################################\n# etcdlib.py -- etcdlib provides a python etcd client\n# author : Bao Li <libao14@pku.edu.cn>, UniAS, SEI, PKU\n# license : BSD License\n############################################################\n\nimport urllib.request, urllib.error\nimport random, json, time\n#import sys\n\n# send http request to etcd server and get the json result \n# url : url\n# data : data to send by POST/PUT\n# method : method used by http request \ndef dorequest(url, data = \"\", method = 'GET'):\n    try: \n        if method == 'GET':\n            response = urllib.request.urlopen(url, timeout=10).read()\n        else:\n            # use PUT/DELETE/POST, data should be encoded in ascii/bytes \n            request = urllib.request.Request(url, data = data.encode('ascii'), method = method)\n            response = urllib.request.urlopen(request, timeout=10).read()\n    # etcd may return json result with response http error code\n    # http error code will raise exception in urlopen\n    # catch the HTTPError and get the json result\n    except urllib.error.HTTPError as e:\n        # e.fp must be read() in this except block.\n        # the e will be deleted and e.fp will be closed after this block\n        response = e.fp.read()\n    # response is encoded in bytes. \n    # recoded in utf-8 and loaded in json\n    result = json.loads(str(response, encoding='utf-8'))\n    return result\n\n\n# client to use etcd\n# not all APIs are implemented below. just implement what we want\nclass Client(object):\n    # server is a string of one server IP and PORT, like 192.168.4.12:2379\n    def __init__(self, server, prefix = \"\"):\n        self.clientid = str(random.random())\n        self.server = \"http://\"+server\n        prefix = prefix.strip(\"/\")\n        if prefix == \"\":\n            self.keysurl = self.server+\"/v2/keys/\"\n        else:\n            self.keysurl = self.server+\"/v2/keys/\"+prefix+\"/\"\n        self.members = self.getmembers()\n\n    def getmembers(self):\n        out = dorequest(self.server+\"/v2/members\")\n        result = []\n        for one in out['members']:\n            result.append(one['clientURLs'][0])\n        return result \n\n    # list etcd servers \n    def listmembers(self):\n        return self.members\n\n    def clean(self):\n        [baseurl, dirname] = self.keysurl.split(\"/v2/keys/\", maxsplit=1)\n        dirname = dirname.strip(\"/\") \n        if dirname == '': # clean root content\n            [status, result] = self.listdir(\"\")\n            if status:\n                for one in result:\n                    if 'dir' in one:\n                        self.deldir(one['key'])\n                    else:\n                        self.delkey(one['key'])\n            if self.isdir(\"_lock\"):\n                self.deldir(\"_lock\")\n        else: # clean a directory\n            if self.isdir(\"\")[0]:\n                self.deldir(\"\")\n            self.createdir(\"\")\n\n    def getkey(self, key):\n        key = key.strip(\"/\")\n        out = dorequest(self.keysurl+key)\n        if 'action' not in out:\n            return [False, \"key not found\"]\n        else:\n            return [True, out['node']['value']]\n\n    def setkey(self, key, value, ttl=0):\n        key = key.strip(\"/\")\n        if ttl == 0:\n            out = dorequest(self.keysurl+key, 'value='+str(value), 'PUT')\n        else:\n            out = dorequest(self.keysurl+key, 'value='+str(value)+\"&ttl=\"+str(ttl), 'PUT')\n        if 'action' not in out:\n            return [False, 'set key failed']\n        else:\n            return [True, out['node']['value']]\n\n    def delkey(self, key):\n        key = key.strip(\"/\")\n        out = dorequest(self.keysurl+key, method='DELETE')\n        if 'action' not in out:\n            return [False, 'delete key failed']\n        else:\n            return [True, out['node']['key']]\n\n    def isdir(self, dirname):\n        dirname = dirname.strip(\"/\") \n        out = dorequest(self.keysurl+dirname)\n        if 'action' not in out:\n            return [False, dirname+\" not found\"]\n        if 'dir' not in out['node']:\n            return [False, dirname+\" is a key\"]\n        return [True, dirname]\n\n    def createdir(self, dirname):\n        dirname = dirname.strip(\"/\")\n        out = dorequest(self.keysurl+dirname, 'dir=true', 'PUT')\n        if 'action' not in out:\n            return [False, 'create dir failed']\n        else:\n            return [True, out['node']['key']]\n    \n    # list key-value in the directory. BUT not recursive.\n    # if necessary, recursive can be supported by add ?recursive=true in url\n    def listdir(self, dirname):\n        dirname = dirname.strip(\"/\")\n        out = dorequest(self.keysurl+dirname)\n        if 'action' not in out:\n            return [False, 'list directory failed']\n        else:\n            if \"dir\" not in out['node']:\n                return [False, dirname+\" is a key\"]\n            if 'nodes' not in out['node']:\n                return [True, []]\n            result=[]\n            for kv in out['node']['nodes']:\n                if 'dir' in kv:\n                    result.append({\"key\":kv['key'], 'dir':True})\n                else:\n                    result.append({\"key\":kv['key'], 'value':kv['value']})\n            return [True, result]\n        \n    # del directory with recursive=true\n    def deldir(self, dirname):\n        dirname = dirname.strip(\"/\")\n        out = dorequest(self.keysurl+dirname+\"?recursive=true\", method='DELETE')\n        if 'action' not in out:\n            return [False, 'delete directory failed']\n        else:\n            return [True, out['node']['key']]\n\n    # watch a key or directory when it changes. \n    # recursive=true means anything in the directory changes, it will return\n    def watch(self, key):\n        key = key.strip(\"/\")\n        out = dorequest(self.keysurl+key+\"?wait=true&recursive=true\")\n        if 'action' not in out:\n            return [False, 'watch key failed']\n        else:\n            return [True, out['node']['value']]\n\n    # atomic create a key. return immediately with True or False\n    def atomiccreate(self, key, value='atom'):\n        key = key.strip(\"/\")\n        out = dorequest(self.keysurl+key+\"?prevExist=false\", 'value='+value, method='PUT')\n        if 'action' not in out:\n            return [False, 'atomic create key failed']\n        else:\n            return [True, out['node']['key']]\n\n    ################# Lock ##################\n    # lockref(key) : get a reference of a lock named key in etcd.\n    #                not need to create this lock. it is automatical.\n    # acquire(lockref) : acquire this lock by lockref.\n    #                    blocked if lock is holded by others\n    # release(lockref) : release this lock by lockref\n    #                    only can be released by holder\n    #########################################\n    def lockref(self, key):\n        key = key.strip(\"/\")\n        return \"_lock/\"+key\n\n    def acquire(self, lockref):\n        while(True):\n            if self.atomiccreate(lockref, self.clientid)[0]:\n                return [True, 'get lock']\n            else:\n                time.sleep(0.01)\n\n    def release(self, lockref):\n        value = self.getkey(lockref)\n        if value[0]:\n            if value[1] == self.clientid:\n                self.delkey(lockref)\n                return [True, 'release lock']\n            else:\n                return [False, 'you are not lock holder']\n        else:\n            return [False, 'no one holds this lock']"
  },
  {
    "path": "src/utils/gputools.py",
    "content": "import lxc\nimport subprocess\nimport os\nimport signal\nfrom utils.log import logger\n\n\n# Note: keep physical device id always the same as the virtual device id\n# device_path e.g. /dev/nvidia0\ndef add_device(container_name, device_path):\n    c = lxc.Container(container_name)\n    return c.add_device_node(device_path, device_path)\n\n\ndef remove_device(container_name, device_path):\n    c = lxc.Container(container_name)\n    return c.remove_device_node('', device_path)\n\n\n# Mon May 21 10:51:45 2018\n# +-----------------------------------------------------------------------------+\n# | NVIDIA-SMI 381.22                 Driver Version: 381.22                    |\n# |-------------------------------+----------------------+----------------------+\n# | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |\n# | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |\n# |===============================+======================+======================|\n# |   0  GeForce GTX 108...  Off  | 0000:02:00.0     Off |                  N/A |\n# | 33%   53C    P2    59W / 250W |    295MiB / 11172MiB |      2%      Default |\n# +-------------------------------+----------------------+----------------------+\n# |   1  GeForce GTX 108...  Off  | 0000:84:00.0     Off |                  N/A |\n# | 21%   35C    P8    10W / 250W |    161MiB / 11172MiB |      0%      Default |\n# +-------------------------------+----------------------+----------------------+\n#\n# +-----------------------------------------------------------------------------+\n# | Processes:                                                       GPU Memory |\n# |  GPU       PID  Type  Process name                               Usage      |\n# |=============================================================================|\n# |    0    111893    C   python3                                        285MiB |\n# |    1    111893    C   python3                                        151MiB |\n# +-----------------------------------------------------------------------------+\n#\ndef nvidia_smi(args=[]):\n    try:\n        cmd = ['nvidia-smi']\n        cmd.extend(args)\n        ret = subprocess.check_output(cmd, stderr=subprocess.STDOUT, timeout=5)\n        return ret.split('\\n')\n    except subprocess.TimeoutExpired:\n        return None\n    except subprocess.CalledProcessError:\n        return None\n    except Exception as e:\n        return None\n\n\ndef get_gpu_driver_version():\n    output = nvidia_smi()\n    if not output:\n        return None\n    else:\n        return output[2].split()[-2]\n\n\n# GPU 0: GeForce GTX 1080 Ti (UUID: GPU-a1c9b91b-5fb2-6059-9784-29ae78cdba8f)\n# GPU 1: GeForce GTX 1080 Ti (UUID: GPU-36a9e2ff-b71d-8601-d0c5-72e0ec72564b)\ndef get_gpu_names():\n    output = nvidia_smi(['-L'])\n    if not output:\n        return []\n    gpu_names = []\n    for line in output:\n        start = line.find(':') + 1\n        end = line.find('(')\n        name = line[start:end].strip().replace(' ', '-')\n        if name:\n            gpu_names.append(name)\n    return gpu_names\n\n\ndef get_gpu_status():\n    output = nvidia_smi()\n    if not output:\n        return []\n    interval_index = [index for index in range(len(output)) if len(output[index].strip()) == 0][0]\n    status_list = []\n    for index in range(7, interval_index, 3):\n        status = {}\n        status['id'] = output[index].split()[1]\n        sp = output[index+1].split()\n        status['fan'] = sp[1]\n        status['memory'] = sp[8]\n        status['memory_max'] = sp[10]\n        status['util'] = sp[12]\n        status_list.append(status)\n    return status_list\n\n\ndef get_gpu_processes():\n    output = nvidia_smi()\n    if not output:\n        return []\n    interval_index = [index for index in range(len(output)) if len(output[index].strip()) == 0][0]\n    process_list = []\n    for index in range(interval_index + 5, len(output)):\n        sp = output[index].split()\n        if len(sp) != 7:\n            break\n        process = {}\n        process['gpu'] = sp[1]\n        process['pid'] = sp[2]\n        process['name'] = sp[4]\n        process['memory'] = sp[5]\n        process['container'] = get_container_name_by_pid(sp[2])\n        process_list.append(process)\n    return process_list\n\n\ndef get_container_name_by_pid(pid):\n    with open('/proc/%s/cgroup' % pid) as f:\n        content = f.readlines()[0].strip().split('/')\n        if content[1] != 'lxc':\n            return 'host'\n        else:\n            return content[2]\n    return None\n\n\ndef clean_up_processes_in_gpu(gpu_id):\n    logger.info('[gputools] start clean up processes in gpu %d' % gpu_id)\n    processes = get_gpu_processes()\n    for process in [p for p in processes if p['gpu'] == gpu_id]:\n        logger.info('[gputools] find process %d running in gpu %d' % (process['pid'], process['gpu']))\n        if process['container'] == 'host':\n            logger.warning('[gputools] find process of host, ignored')\n        else:\n            logger.warning('[gputools] find process of container [%s], killed' % process['container'])\n            try:\n                os.kill(process['pid'], signal.SIGKILL)\n            except OSError:\n                continue\n"
  },
  {
    "path": "src/utils/imagemgr.py",
    "content": "#!/usr/bin/python3\n\n\"\"\"\ndesign:\n    1. When user create an image, it will upload to an image server, at the same time, local host\n    will save an image. A time file will be made with them. Everytime a container start by this\n    image, the time file will update.\n    2. When user save an image, if it is a update option, it will faster than create a new image.\n    3. At image server and every physical host, run a shell script to delete the image, which is\n    out of time.\n    4. We can show every user their own images and the images are shared by other. User can new a\n    cluster or scale out a new node by them. And user can remove his own images.\n    5. When a remove option occur, the image server will delete it. But some physical host may\n    also maintain it. I think it doesn't matter.\n    6. The manage of lvm has been including in this module.\n\"\"\"\n\n\nfrom configparser import ConfigParser\nfrom io import StringIO\nimport os,sys,subprocess,time,re,datetime,threading,random\nimport xmlrpc.client\nfrom utils.model import db, Image\n\nfrom utils.log import logger\nfrom utils import env, updatebase\nfrom utils.lvmtool import *\nimport requests\n\nmaster_port = str(env.getenv('MASTER_PORT'))\n\nclass ImageMgr():\n    #def sys_call(self,command):\n    #    output = subprocess.getoutput(command).strip()\n    #    return None if output == '' else output\n\n    def sys_return(self,command):\n        return_value = subprocess.call(command,shell=True)\n        return return_value\n\n    def __init__(self):\n        self.NFS_PREFIX = env.getenv('FS_PREFIX')\n        self.imgpath = self.NFS_PREFIX + \"/global/images/\"\n        self.srcpath = env.getenv('DOCKLET_LIB') + \"/\"\n        self.imageserver = \"192.168.6.249\"\n\n    def datetime_toString(self,dt):\n        return dt.strftime(\"%Y-%m-%d %H:%M:%S\")\n\n    def string_toDatetime(self,string):\n        return datetime.datetime.strptime(string, \"%Y-%m-%d %H:%M:%S\")\n\n    def updateinfo(self,user,imagename,description):\n        '''image_info_file = open(imgpath+\".\"+image+\".info\",'w')\n        image_info_file.writelines([self.datetime_toString(datetime.datetime.now()) + \"\\n\", \"unshare\"])\n        image_info_file.close()\n        image_description_file = open(imgpath+\".\"+image+\".description\", 'w')\n        image_description_file.write(description)\n        image_description_file.close()'''\n        image = Image.query.filter_by(ownername=user,imagename=imagename).first()\n        if image is None:\n            newimage = Image(imagename,True,False,user,description)\n            db.session.add(newimage)\n            db.session.commit()\n\n\n    def dealpath(self,fspath):\n        if fspath[-1:] == \"/\":\n            return self.dealpath(fspath[:-1])\n        else:\n            return fspath\n\n    def createImage(self,user,image,lxc,description=\"Not thing\", imagenum=10):\n        fspath = self.NFS_PREFIX + \"/local/volume/\" + lxc\n        imgpath = self.imgpath + \"private/\" + user + \"/\"\n        #tmppath = self.NFS_PREFIX + \"/local/tmpimg/\"\n        #tmpimage = str(random.randint(0,10000000)) + \".tz\"\n\n        if not os.path.exists(imgpath+image) and os.path.exists(imgpath):\n            cur_imagenum = 0\n            for filename in os.listdir(imgpath):\n                if os.path.isdir(imgpath+filename):\n                    cur_imagenum += 1\n            if cur_imagenum >= int(imagenum):\n                return [False,\"image number limit exceeded\"]\n        #sys_run(\"mkdir -p %s\" % tmppath, True)\n        sys_run(\"mkdir -p %s\" % imgpath,True)\n        try:\n            sys_run(\"tar -cvf %s -C %s .\" % (imgpath+image+\".tz\",self.dealpath(fspath)), True)\n        except Exception as e:\n            logger.error(e)\n        #try:\n            #sys_run(\"cp %s %s\" % (tmppath+tmpimage, imgpath+image+\".tz\"), True)\n            #sys_run(\"rsync -a --delete --exclude=lost+found/ --exclude=root/nfs/ --exclude=dev/ --exclude=mnt/ --exclude=tmp/ --exclude=media/ --exclude=proc/ --exclude=sys/ %s/ %s/\" % (self.dealpath(fspath),imgpath+image),True)\n        #except Exception as e:\n        #    logger.error(e)\n        #sys_run(\"rm -f %s\" % tmppath+tmpimage, True)\n        #sys_run(\"rm -f %s\" % (imgpath+\".\"+image+\"_docklet_share\"),True)\n        self.updateinfo(user,image,description)\n        logger.info(\"image:%s from LXC:%s create success\" % (image,lxc))\n        return [True, \"create image success\"]\n\n    def prepareImage(self,user,image,fspath):\n        imagename = image['name']\n        imagetype = image['type']\n        imageowner = image['owner']\n        #tmppath = self.NFS_PREFIX + \"/local/tmpimg/\"\n        #tmpimage = str(random.randint(0,10000000)) + \".tz\"\n        if imagename == \"base\" and imagetype == \"base\":\n            return\n        if imagetype == \"private\":\n            imgpath = self.imgpath + \"private/\" + user + \"/\"\n        else:\n            imgpath = self.imgpath + \"public/\" + imageowner + \"/\"\n        #try:\n        #    sys_run(\"cp %s %s\" % (imgpath+imagename+\".tz\", tmppath+tmpimage))\n        #except Exception as e:\n        #    logger.error(e)\n        try:\n            sys_run(\"tar -C %s -xvf %s\" % (self.dealpath(fspath),imgpath+imagename+\".tz\"), True)\n            #sys_run(\"rsync -a --delete --exclude=lost+found/ --exclude=root/nfs/ --exclude=dev/ --exclude=mnt/ --exclude=tmp/ --exclude=media/ --exclude=proc/ --exclude=sys/ %s/ %s/\" % (imgpath+imagename,self.dealpath(fspath)),True)\n        except Exception as e:\n            logger.error(e)\n        #sys_run(\"rm -f %s\" % tmppath+tmpimage)\n\n        #self.sys_call(\"rsync -a --delete --exclude=nfs/ %s/ %s/\" % (imgpath+image,self.dealpath(fspath)))\n        #self.updatetime(imgpath,image)\n        return\n\n    def prepareFS(self,user,image,lxc,size=\"1000\",vgname=\"docklet-group\"):\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxc\n        layer = self.NFS_PREFIX + \"/local/volume/\" + lxc\n        #check mountpoint\n        Ret = sys_run(\"mountpoint %s\" % rootfs)\n        if Ret.returncode == 0:\n            logger.info(\"%s not clean\" % rootfs)\n            sys_run(\"umount -l %s\" % rootfs)\n        Ret = sys_run(\"mountpoint %s\" % layer)\n        if Ret.returncode == 0:\n            logger.info(\"%s not clean\" % layer)\n            sys_run(\"umount -l %s\" % layer)\n\n        try:\n            sys_run(\"rm -rf %s %s\" % (rootfs, layer))\n            sys_run(\"mkdir -p %s %s\" % (rootfs, layer))\n        except Exception as e:\n            logger.error(e)\n\n\n        #prepare volume\n        if check_volume(vgname,lxc):\n            logger.info(\"volume %s already exists, delete it\")\n            delete_volume(vgname,lxc)\n        if not new_volume(vgname,lxc,size):\n            logger.error(\"volume %s create failed\" % lxc)\n            return False\n\n        try:\n            sys_run(\"mkfs.ext4 /dev/%s/%s\" % (vgname,lxc),True)\n            sys_run(\"mount /dev/%s/%s %s\" %(vgname,lxc,layer),True)\n            #self.sys_call(\"mkdir -p %s/overlay %s/work\" % (layer,layer))\n            #self.sys_call(\"mount -t overlay overlay -olowerdir=%s/local/basefs,upperdir=%s/overlay,workdir=%s/work %s\" % (self.NFS_PREFIX,layer,layer,rootfs))\n            #self.prepareImage(user,image,layer+\"/overlay\")\n            self.prepareImage(user,image,layer)\n            logger.info(\"image has been prepared\")\n            sys_run(\"mount -t aufs -o br=%s=rw:%s/local/packagefs=ro+wh:%s/local/basefs=ro+wh -o udba=reval none %s/\" % (layer,self.NFS_PREFIX,self.NFS_PREFIX,rootfs),True)\n            sys_run(\"mkdir -m 777 -p %s/local/temp/%s\" % (self.NFS_PREFIX,lxc))\n\n        except Exception as e:\n            logger.error(e)\n\n        logger.info(\"FS has been prepared for user:%s lxc:%s\" % (user,lxc))\n        return True\n\n    def deleteFS(self,lxc,vgname=\"docklet-group\"):\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxc\n        layer = self.NFS_PREFIX + \"/local/volume/\" + lxc\n        lxcpath = \"/var/lib/lxc/%s\" % lxc\n        sys_run(\"lxc-stop -k -n %s\" % lxc)\n        #check mountpoint\n        Ret = sys_run(\"mountpoint %s\" % rootfs)\n        if Ret.returncode == 0:\n            sys_run(\"umount -l %s\" % rootfs)\n        Ret = sys_run(\"mountpoint %s\" % layer)\n        if Ret.returncode == 0:\n            sys_run(\"umount -l %s\" % layer)\n        if check_volume(vgname, lxc):\n            delete_volume(vgname, lxc)\n        try:\n            sys_run(\"rm -rf %s %s\" % (layer,lxcpath))\n            sys_run(\"rm -rf %s/local/temp/%s\" % (self.NFS_PREFIX,lxc))\n        except Exception as e:\n            logger.error(e)\n\n        return True\n\n    def detachFS(self, lxc, vgname=\"docklet-group\"):\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxc\n        Ret = sys_run(\"umount %s\" % rootfs)\n        if Ret.returncode != 0:\n            logger.error(\"cannot umount rootfs:%s\" % rootfs)\n            return False\n        return True\n\n    def checkFS(self, lxc, vgname=\"docklet-group\"):\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxc\n        layer = self.NFS_PREFIX + \"/local/volume/\" + lxc\n        if not os.path.isdir(layer):\n            sys_run(\"mkdir -p %s\" % layer)\n        #check mountpoint\n        Ret = sys_run(\"mountpoint %s\" % layer)\n        if Ret.returncode != 0:\n            sys_run(\"mount /dev/%s/%s %s\" % (vgname,lxc,layer))\n        Ret = sys_run(\"mountpoint %s\" % rootfs)\n        if Ret.returncode != 0:\n            sys_run(\"mount -t aufs -o br=%s=rw:%s/local/packagefs=ro+wh:%s/local/basefs=ro+wh -o udba=reval none %s/\" % (layer,self.NFS_PREFIX,self.NFS_PREFIX,rootfs))\n        return True\n\n\n    def removeImage(self,user,imagename):\n        imgpath = self.imgpath + \"private/\" + user + \"/\"\n        try:\n            image = Image.query.filter_by(imagename=imagename,ownername=user).first()\n            image.hasPrivate = False\n            if image.hasPublic == False:\n                db.session.delete(image)\n            db.session.commit()\n            sys_run(\"rm -rf %s/\" % imgpath+imagename+\".tz\", True)\n            #sys_run(\"rm -f %s\" % imgpath+\".\"+image+\".info\", True)\n            #sys_run(\"rm -f %s\" % (imgpath+\".\"+image+\".description\"), True)\n        except Exception as e:\n            logger.error(e)\n\n    def shareImage(self,user,imagename):\n        imgpath = self.imgpath + \"private/\" + user + \"/\"\n        share_imgpath = self.imgpath + \"public/\" + user + \"/\"\n        '''image_info_file = open(imgpath+\".\"+image+\".info\", 'r')\n        [createtime, isshare] = image_info_file.readlines()\n        isshare = \"shared\"\n        image_info_file.close()\n        image_info_file = open(imgpath+\".\"+image+\".info\", 'w')\n        image_info_file.writelines([createtime, isshare])\n        image_info_file.close()'''\n        try:\n            image = Image.query.filter_by(imagename=imagename,ownername=user).first()\n            if image.hasPublic == True:\n                return\n            image.hasPublic = True\n            db.session.commit()\n            sys_run(\"mkdir -p %s\" % share_imgpath, True)\n            sys_run(\"cp %s %s\" % (imgpath+imagename+\".tz\", share_imgpath+imagename+\".tz\"), True)\n            #sys_run(\"rsync -a --delete %s/ %s/\" % (imgpath+image,share_imgpath+image), True)\n        except Exception as e:\n            logger.error(e)\n        #$sys_run(\"cp %s %s\" % (imgpath+\".\"+image+\".info\",share_imgpath+\".\"+image+\".info\"), True)\n        #sys_run(\"cp %s %s\" % (imgpath+\".\"+image+\".description\",share_imgpath+\".\"+image+\".description\"), True)\n\n\n\n    def unshareImage(self,user,imagename):\n        public_imgpath = self.imgpath + \"public/\" + user + \"/\"\n        imgpath = self.imgpath + \"private/\" + user + \"/\"\n        '''if os.path.isfile(imgpath + image + \".tz\"):\n            image_info_file = open(imgpath+\".\"+image+\".info\", 'r')\n            [createtime, isshare] = image_info_file.readlines()\n            isshare = \"unshare\"\n            image_info_file.close()\n            image_info_file = open(imgpath+\".\"+image+\".info\", 'w')\n            image_info_file.writelines([createtime, isshare])\n            image_info_file.close()'''\n        try:\n            #sys_run(\"rm -rf %s/\" % public_imgpath+image, True)\n            image = Image.query.filter_by(imagename=imagename,ownername=user).first()\n            image.hasPublic = False\n            if image.hasPrivate == False:\n                db.session.delete(image)\n            db.session.commit()\n            sys_run(\"rm -f %s\" % public_imgpath+imagename+\".tz\", True)\n            #sys_run(\"rm -f %s\" % public_imgpath+\".\"+image+\".info\", True)\n            #sys_run(\"rm -f %s\" % public_imgpath+\".\"+image+\".description\", True)\n        except Exception as e:\n            logger.error(e)\n\n    def copyImage(self,user,image,token,target):\n        path = \"/opt/docklet/global/images/private/\"+user+\"/\"\n        '''image_info_file = open(path+\".\"+image+\".info\", 'r')\n        [createtime, isshare] = image_info_file.readlines()\n        recordshare = isshare\n        isshare = \"unshared\"\n        image_info_file.close()\n        image_info_file = open(path+\".\"+image+\".info\", 'w')\n        image_info_file.writelines([createtime, isshare])\n        image_info_file.close()'''\n        try:\n            sys_run('ssh root@%s \"mkdir -p %s\"' % (target,path))\n            sys_run('scp %s%s.tz root@%s:%s' % (path,image,target,path))\n            #sys_run('scp %s.%s.description root@%s:%s' % (path,image,target,path))\n            #sys_run('scp %s.%s.info root@%s:%s' % (path,image,target,path))\n            resimage = Image.query.filter_by(ownername=user,imagename=image).first()\n            auth_key = env.getenv('AUTH_KEY')\n            url = \"http://\" + target + \":\" + master_port + \"/image/copytarget/\"\n            data = {\"token\":token,\"auth_key\":auth_key,\"user\":user,\"imagename\":image,\"description\":resimage.description}\n            result = requests.post(url, data=data).json()\n            logger.info(\"Response from target master: \" + str(result))\n        except Exception as e:\n            logger.error(e)\n            '''image_info_file = open(path+\".\"+image+\".info\", 'w')\n            image_info_file.writelines([createtime, recordshare])\n            image_info_file.close()'''\n            return {'success':'false', 'message':str(e)}\n        '''image_info_file = open(path+\".\"+image+\".info\", 'w')\n        image_info_file.writelines([createtime, recordshare])\n        image_info_file.close()'''\n        logger.info(\"copy image %s of %s to %s success\" % (image,user,target))\n        return {'success':'true', 'action':'copy image'}\n\n    def update_basefs(self,imagename):\n        imgpath = self.imgpath + \"private/root/\"\n        basefs = self.NFS_PREFIX+\"/local/packagefs/\"\n        tmppath = self.NFS_PREFIX + \"/local/tmpimg/\"\n        tmpimage = str(random.randint(0,10000000))\n        try:\n            sys_run(\"mkdir -p %s\" % tmppath+tmpimage)\n            sys_run(\"tar -C %s -xvf %s\" % (tmppath+tmpimage,imgpath+imagename+\".tz\"),True)\n            logger.info(\"start updating base image\")\n            updatebase.aufs_update_base(tmppath+tmpimage, basefs)\n            logger.info(\"update base image success\")\n        except Exception as e:\n            logger.error(e)\n        sys_run(\"rm -rf %s\" % tmppath+tmpimage)\n        return True\n\n    def update_base_image(self, user, vclustermgr, image):\n        if not user == \"root\":\n            logger.info(\"only root can update base image\")\n        #vclustermgr.stop_allclusters()\n        #vclustermgr.detach_allclusters()\n        workers = vclustermgr.nodemgr.get_nodeips()\n        logger.info(\"update base image in all workers\")\n        for worker in workers:\n            workerrpc = vclustermgr.nodemgr.ip_to_rpc(worker)\n            workerrpc.update_basefs(image)\n        logger.info(\"update base image success\")\n        #vclustermgr.mount_allclusters()\n        #logger.info(\"mount all cluster success\")\n        #vclustermgr.recover_allclusters()\n        #logger.info(\"recover all cluster success\")\n        return [True, \"update base image\"]\n\n    def get_image_info(self, user, imagename, imagetype):\n        '''if imagetype == \"private\":\n            imgpath = self.imgpath + \"private/\" + user + \"/\"\n        else:\n            imgpath = self.imgpath + \"public/\" + user + \"/\"\n        image_info_file = open(imgpath+\".\"+image+\".info\",'r')\n        time = image_info_file.readline()\n        image_info_file.close()\n        image_description_file = open(imgpath+\".\"+image+\".description\",'r')\n        description = image_description_file.read()\n        image_description_file.close()'''\n        image = Image.query.filter_by(imagename=imagename,ownername=user).first()\n        if image is None:\n            return [\"\", \"\"]\n        time = image.create_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        description = image.description\n        if len(description) > 15:\n            description = description[:15] + \"......\"\n        return [time, description]\n\n    def get_image_description(self, user, image):\n        '''if image['type'] == \"private\":\n            imgpath = self.imgpath + \"private/\" + user + \"/\"\n        else:\n            imgpath = self.imgpath + \"public/\" + image['owner'] + \"/\"\n        image_description_file = open(imgpath+\".\"+image['name']+\".description\", 'r')\n        description = image_description_file.read()\n        image_description_file.close()'''\n        image = Image.query.filter_by(imagename=image['name'],ownername=image['owner']).first()\n        if image is None:\n            return \"\"\n        return image.description\n\n    def get_image_size(self, image):\n        imagename = image['name']\n        imagetype = image['type']\n        imageowner = image['owner']\n        if imagename == \"base\" and imagetype == \"base\":\n            return 0\n        if imagetype == \"private\":\n            imgpath = self.imgpath + \"private/\" + imageowner + \"/\"\n        else:\n            imgpath = self.imgpath + \"public/\" + imageowner + \"/\"\n        return os.stat(os.path.join(imgpath, imagename+\".tz\")).st_size // (1024*1024)\n\n\n    def format_size(self, size_in_byte):\n        if size_in_byte < 1024:\n            return str(size_in_byte) + \"B\"\n        elif size_in_byte < 1024*1024:\n            return str(size_in_byte//1024) + \"KB\"\n        elif size_in_byte < 1024*1024*1024:\n            return str(size_in_byte//(1024*1024)) + \"MB\"\n        else:\n            return str(size_in_byte//(1024*1024*1024)) + \"GB\"\n\n    def list_images(self,user):\n        images = {}\n        images[\"private\"] = []\n        images[\"public\"] = {}\n        imgpath = self.imgpath + \"private/\" + user + \"/\"\n        try:\n            Ret = sys_run(\"ls %s\" % imgpath, True)\n            private_images = str(Ret.stdout,\"utf-8\").split()\n            for image in private_images:\n                if not image[-3:] == '.tz':\n                    continue\n                imagename = image[:-3]\n                fimage={}\n                fimage[\"name\"] = imagename\n                fimage[\"isshared\"] = self.isshared(user,imagename)\n                [time, description] = self.get_image_info(user, imagename, \"private\")\n                fimage[\"time\"] = time\n                fimage[\"description\"] = description\n                fimage[\"size\"] = os.stat(os.path.join(imgpath, image)).st_size\n                fimage[\"size_format\"] = self.format_size(fimage[\"size\"])\n                fimage[\"size_in_mb\"] = fimage[\"size\"] // (1024*1024)\n                images[\"private\"].append(fimage)\n        except Exception as e:\n            logger.error(e)\n\n        imgpath = self.imgpath + \"public\" + \"/\"\n        try:\n            Ret = sys_run(\"ls %s\" % imgpath, True)\n            public_users = str(Ret.stdout,\"utf-8\").split()\n            for public_user in public_users:\n                imgpath = self.imgpath + \"public/\" + public_user + \"/\"\n                try:\n                    Ret = sys_run(\"ls %s\" % imgpath, True)\n                    public_images = str(Ret.stdout,\"utf-8\").split()\n                    if len(public_images)==0:\n                        continue\n                    images[\"public\"][public_user] = []\n                    for image in public_images:\n                        if not image[-3:] == '.tz':\n                            continue\n                        imagename = image[:-3]\n                        fimage = {}\n                        fimage[\"name\"] = imagename\n                        [time, description] = self.get_image_info(public_user, imagename, \"public\")\n                        fimage[\"time\"] = time\n                        fimage[\"description\"] = description\n                        fimage[\"size\"] = os.stat(os.path.join(imgpath, image)).st_size\n                        fimage[\"size_format\"] = self.format_size(fimage[\"size\"])\n                        fimage[\"size_in_mb\"] = fimage[\"size\"] // (1024*1024)\n                        images[\"public\"][public_user].append(fimage)\n                except Exception as e:\n                    logger.error(e)\n        except Exception as e:\n            logger.error(e)\n\n        return images\n\n    def isshared(self,user,imagename):\n        '''imgpath = self.imgpath + \"private/\" + user + \"/\"\n        image_info_file = open(imgpath+\".\"+image+\".info\",'r')\n        [time, isshare] = image_info_file.readlines()\n        image_info_file.close()'''\n        image = Image.query.filter_by(imagename=imagename,ownername=user).first()\n        if image is None:\n            return \"\"\n        if image.hasPublic == True:\n            return \"true\"\n        else:\n            return \"false\"\n\nif __name__ == '__main__':\n    mgr = ImageMgr()\n    if sys.argv[1] == \"prepareImage\":\n        mgr.prepareImage(sys.argv[2],sys.argv[3],sys.argv[4])\n    elif sys.argv[1] == \"create\":\n        mgr.createImage(sys.argv[2],sys.argv[3],sys.argv[4])\n    else:\n        logger.warning(\"unknown option\")\n"
  },
  {
    "path": "src/utils/log.py",
    "content": "#!/usr/bin/env python\n\nimport logging\nimport logging.handlers\nimport argparse\nimport sys\nimport time  # this is only being used as part of the example\nimport os\nfrom utils import env\n\n# logger should only be imported after initlogging has been called\nlogger = None\n\ndef initlogging(name='docklet'):\n    # Deafults\n    global logger\n\n    homepath = env.getenv('FS_PREFIX')\n    LOG_FILENAME = homepath + '/local/log/' + name + '.log'\n\n    LOG_LIFE = env.getenv('LOG_LIFE')\n    LOG_LEVEL = env.getenv('LOG_LEVEL')\n    if LOG_LEVEL == \"DEBUG\":\n        LOG_LEVEL = logging.DEBUG\n    elif LOG_LEVEL == \"INFO\":\n        LOG_LEVEL = logging.INFO\n    elif LOG_LEVEL == \"WARNING\":\n        LOG_LEVEL = logging.WARNING\n    elif LOG_LEVEL == \"ERROR\":\n        LOG_LEVEL = logging.ERROR\n    elif LOG_LEVEL == \"CRITICAL\":\n        LOG_LEVEL = logging.CRITIAL\n    else:\n        LOG_LEVEL = logging.DEBUG\n    logger = logging.getLogger(name)\n    # Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data\n    # Give the logger a unique name (good practice)\n    # Set the log level to LOG_LEVEL\n    logger.setLevel(LOG_LEVEL)\n    # Make a handler that writes to a file, making a new file at midnight and keeping 3 backups\n    handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME,\n            when=\"midnight\", backupCount=LOG_LIFE, encoding='utf-8')\n    # Format each log message like this\n    formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(module)s[%(lineno)d] %(message)s')\n    # Attach the formatter to the handler\n    handler.setFormatter(formatter)\n    # Attach the handler to the logger\n    logger.addHandler(handler)\n    # Replace stdout with logging to file at INFO level\n    sys.stdout = RedirectLogger(logger, logging.INFO)\n    # Replace stderr with logging to file at ERROR level\n    sys.stderr = RedirectLogger(logger, logging.ERROR)\n\n    # Make a class we can use to capture stdout and sterr in the log\nclass RedirectLogger(object):\n    def __init__(self, logger, level):\n        \"\"\"Needs a logger and a logger level.\"\"\"\n        self.logger = logger\n        self.level = level\n\n    def write(self, message):\n        # Only log if there is a message (not just a new line)\n        if message.rstrip() != \"\":\n            self.logger.log(self.level, message.rstrip())\n\n    def flush(self):\n        for handler in self.logger.handlers:\n            handler.flush()\n"
  },
  {
    "path": "src/utils/logs.py",
    "content": "#!/usr/bin/python3\n\nfrom utils import env\nimport json, os\nfrom utils.log import logger\nfrom werkzeug.utils import secure_filename\n\nlogsPath = env.getenv('FS_PREFIX') + '/local/log/'\n\nclass logsClass:\n    setting = {}\n\n    def list(*args, **kwargs):\n        if ( ('user_group' in kwargs) == False):\n            return {\"success\":'false', \"reason\":\"Cannot get user_group\"}\n        user_group = kwargs['user_group']\n        if (not ((user_group == 'admin') or (user_group == 'root'))):\n            return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n        s = os.listdir(logsPath)\n        r = []\n        for i in s:\n            if ('log' in i):\n                r.append(i)\n        return {'success': 'true', 'result': r}\n\n    def get(*args, **kwargs):\n        if ( ('user_group' in kwargs) == False):\n            return {\"success\":'false', \"reason\":\"Cannot get user_group\"}\n        user_group = kwargs['user_group']\n        if (not ((user_group == 'admin') or (user_group == 'root'))):\n            return {\"success\": 'false', \"reason\": 'Unauthorized Action'}\n        filepath = logsPath + secure_filename(kwargs['filename'])\n        try:\n            if not os.path.exists(filepath):\n                return {\"success\": 'false', \"reason\": 'file not exist'}\n            logfile = open(filepath, 'r')\n            logtext = logfile.read()\n            logfile.close()\n            return {'success': 'true', 'result': logtext}\n        except:\n            return {'success': 'false', 'reason': 'file read error'}\n\n\n\nlogs = logsClass()\n"
  },
  {
    "path": "src/utils/lvmtool.py",
    "content": "#!/usr/bin/python3\n\nimport subprocess,os,time\nfrom utils.log import logger\nfrom utils import env\n\ndef sys_run(command,check=False):\n    Ret = subprocess.run(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, shell=True, check=check)\n    return Ret\n\ndef new_group(group_name, size = \"5000\", file_path = \"/opt/docklet/local/docklet-storage\"):\n    storage = env.getenv(\"STORAGE\")\n    logger.info(\"begin initialize lvm group:%s with size %sM\" % (group_name,size))\n    if storage == \"file\":\n        #check vg\n        Ret = sys_run(\"vgdisplay \" + group_name)\n        if Ret.returncode == 0:\n            logger.info(\"lvm group: \" + group_name + \" already exists, delete it\")\n            Ret = sys_run(\"vgremove -f \" + group_name)\n            if Ret.returncode != 0:\n                logger.error(\"delete VG %s failed:%s\" % (group_name,Ret.stdout.decode('utf-8')))\n        #check pv\n        Ret = sys_run(\"pvdisplay /dev/loop0\")\n        if Ret.returncode == 0:\n            Ret = sys_run(\"pvremove -ff /dev/loop0\")\n            if Ret.returncode != 0:\n                logger.error(\"remove pv failed:%s\" % Ret.stdout.decode('utf-8'))\n        #check mountpoint\n        Ret = sys_run(\"losetup /dev/loop0\")\n        if Ret.returncode == 0:\n            logger.info(\"/dev/loop0 already exists, detach it\")\n            Ret = sys_run(\"losetup -d /dev/loop0\")\n            if Ret.returncode != 0:\n                logger.error(\"losetup -d failed:%s\" % Ret.stdout.decode('utf-8'))\n        #check file_path\n        if os.path.exists(file_path):\n            logger.info(file_path + \" for lvm group already exists, delete it\")\n            os.remove(file_path)\n        if not os.path.isdir(file_path[:file_path.rindex(\"/\")]):\n            os.makedirs(file_path[:file_path.rindex(\"/\")])\n        try:\n            sys_run(\"dd if=/dev/zero of=%s bs=1M seek=%s count=0\" % (file_path,size))\n            sys_run(\"losetup /dev/loop0 \" + file_path)\n            sys_run(\"vgcreate %s /dev/loop0\" % group_name)\n        except Exception as e:\n            logger.error(e)\n        logger.info(\"initialize lvm group:%s with size %sM success\" % (group_name,size))\n        return True\n\n    elif storage == \"disk\":\n        disk = env.getenv(\"DISK\")\n        if disk is None:\n            logger.error(\"use disk for story without a physical disk\")\n            return False\n        #check vg\n        Ret = sys_run(\"vgdisplay \" + group_name)\n        if Ret.returncode == 0:\n            logger.info(\"lvm group: \" + group_name + \" already exists, delete it\")\n            Ret = sys_run(\"vgremove -f \" + group_name)\n            if Ret.returncode != 0:\n                logger.error(\"delete VG %s failed:%s\" % (group_name,Ret.stdout.decode('utf-8')))\n        try:\n            sys_run(\"vgcreate %s %s\" % (group_name,disk))\n        except Exception as e:\n            logger.error(e)\n        logger.info(\"initialize lvm group:%s with size %sM success\" % (group_name,size))\n        return True\n\n    else:\n        logger.info(\"unknown storage type:\" + storage)\n        return False\n\ndef recover_group(group_name,file_path=\"/opt/docklet/local/docklet-storage\"):\n    storage = env.getenv(\"STORAGE\")\n    if storage == \"file\":\n        if not os.path.exists(file_path):\n            logger.error(\"%s not found, unable to recover VG\" % file_path)\n            return False\n        #recover mountpoint\n        Ret = sys_run(\"losetup /dev/loop0\")\n        if Ret.returncode != 0:\n            Ret = sys_run(\"losetup /dev/loop0 \" + file_path)\n            if Ret.returncode != 0:\n                logger.error(\"losetup failed:%s\" % Ret.stdout.decode('utf-8'))\n                return False\n        time.sleep(1)\n        #recover vg\n        Ret = sys_run(\"vgdisplay \" + group_name)\n        if Ret.returncode != 0:\n            Ret = sys_run(\"vgcreate %s /dev/loop0\" % group_name)\n            if Ret.returncode != 0:\n                logger.error(\"create VG %s failed:%s\" % (group_name,Ret.stdout.decode('utf-8')))\n                return False\n        logger.info(\"recover VG %s success\" % group_name)\n\n    elif storage == \"disk\":\n        disk = env.getenv(\"DISK\")\n        if disk is None:\n            logger.error(\"use disk for story without a physical disk\")\n            return False\n        #recover vg\n        Ret = sys_run(\"vgdisplay \" + group_name)\n        if Ret.returncode != 0:\n            Ret = sys_run(\"vgcreate %s %s\" % (group_name,disk))\n            if Ret.returncode != 0:\n                logger.error(\"create VG %s failed:%s\" % (group_name,Ret.stdout.decode('utf-8')))\n                return False\n        logger.info(\"recover VG %s success\" % group_name)\n\ndef new_volume(group_name,volume_name,size):\n    Ret = sys_run(\"lvdisplay %s/%s\" % (group_name,volume_name))\n    if Ret.returncode == 0:\n        logger.info(\"logical volume already exists, delete it\")\n        Ret = sys_run(\"lvremove -f %s/%s\" % (group_name,volume_name))\n        if Ret.returncode != 0:\n            logger.error(\"delete logical volume %s failed: %s\" %\n                    (volume_name, Ret.stdout.decode('utf-8')))\n    Ret = sys_run(\"lvcreate -L %sM -n %s %s\" % (size,volume_name,group_name))\n    if Ret.returncode != 0:\n        logger.error(\"lvcreate failed: %s\" % Ret.stdout.decode('utf-8'))\n        return False\n    logger.info(\"create lv success\")\n    return True\n\ndef check_group(group_name):\n    Ret = sys_run(\"vgdisplay %s\" % group_name)\n    if Ret.returncode == 0:\n        return True\n    else:\n        return False\n\ndef check_volume(group_name,volume_name):\n    Ret = sys_run(\"lvdisplay %s/%s\" % (group_name,volume_name))\n    if Ret.returncode == 0:\n        return True\n    else:\n        return False\n\ndef delete_group(group_name):\n    Ret = sys_run(\"vgdisplay %s\" % group_name)\n    if Ret.returncode == 0:\n        Ret = sys_run(\"vgremove -f %s\" % group_name)\n        if Ret.returncode == 0:\n            logger.info(\"delete vg %s success\" % group_name)\n            return True\n        else:\n            logger.error(\"delete vg %s failed:%s\" % (group_name,Ret.stdout.decode('utf-8')))\n            return False\n    else:\n        logger.info(\"vg %s does not exists\" % group_name)\n        return True\n\ndef delete_volume(group_name, volume_name):\n    Ret = sys_run(\"lvdisplay %s/%s\" % (group_name, volume_name))\n    if Ret.returncode == 0:\n        Ret = sys_run(\"lvremove -f %s/%s\" % (group_name, volume_name))\n        if Ret.returncode == 0:\n            logger.info(\"delete lv %s in vg %s success\" % (volume_name,group_name))\n            return True\n        else:\n            logger.error(\"delete lv %s in vg %s failed:%s\" % (volume_name,group_name,Ret.stdout.decode('utf-8')))\n            return False\n    else:\n        logger.info(\"lv %s in vg %s does not exists\" % (volume_name,group_name))\n"
  },
  {
    "path": "src/utils/manage.py",
    "content": "import sys\nif sys.path[0].endswith(\"utils\"):\n    sys.path[0] = sys.path[0][:-5]\nfrom flask_migrate import Migrate,MigrateCommand\nfrom utils.model import *\nfrom flask_script import Manager\nfrom flask import Flask\n\nmigrate = Migrate(app,db)\nmanager = Manager(app)\nmanager.add_command('db',MigrateCommand)\n\nif __name__ == '__main__':\n    manager.run()\n"
  },
  {
    "path": "src/utils/model.py",
    "content": "#coding=utf-8\n'''\n2 tables: users, usergroup\nUser:\n    id\n    username\n    password\n    avatar\n    nickname\n    description\n    status\n    student_number\n    department\n    truename\n    tel\n    e_mail\n    register_date\n    user_group\n    auth_method\n\nUsergroup\n    id\n    name\n\nToken expiration can be set in User.generate_auth_token\n'''\nfrom flask import Flask\nfrom flask_sqlalchemy import SQLAlchemy\nfrom datetime import datetime\nfrom base64 import b64encode, b64decode\nimport os, json\n\n#this class from itsdangerous implements token<->user\nfrom itsdangerous import TimedJSONWebSignatureSerializer as Serializer\nfrom itsdangerous import SignatureExpired, BadSignature\n\nfrom utils import env\n\nfsdir = env.getenv('FS_PREFIX')\n\napp = Flask(__name__)\napp.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///'+fsdir+'/global/sys/UserTable.db'\napp.config['SQLALCHEMY_BINDS'] = {\n    'history': 'sqlite:///'+fsdir+'/global/sys/HistoryTable.db',\n    'beansapplication': 'sqlite:///'+fsdir+'/global/sys/BeansApplication.db',\n    'system': 'sqlite:///'+fsdir+'/global/sys/System.db',\n    'batch':'sqlite:///'+fsdir+'/global/sys/Batch.db?check_same_thread=False',\n    'login': 'sqlite:///'+fsdir+'/global/sys/Login.db'\n    }\napp.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True\ntry:\n    secret_key_file = open(env.getenv('FS_PREFIX') + '/local/token_secret_key.txt')\n    app.secret_key = secret_key_file.read()\n    secret_key_file.close()\nexcept:\n    from os import urandom\n    secret_key = urandom(24)\n    secret_key = b64encode(secret_key).decode('utf-8')\n    app.secret_key = secret_key\n    secret_key_file = open(env.getenv('FS_PREFIX') + '/local/token_secret_key.txt', 'w')\n    secret_key_file.write(secret_key)\n    secret_key_file.close()\n\ndb = SQLAlchemy(app)\n\nclass User(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(10), unique=True)\n    password = db.Column(db.String(100))\n    avatar = db.Column(db.String(30))\n    nickname = db.Column(db.String(10))\n    description = db.Column(db.String(15))\n    status = db.Column(db.String(10))\n    e_mail = db.Column(db.String(20))\n    student_number = db.Column(db.String(20))\n    department = db.Column(db.String(20))\n    truename = db.Column(db.String(20))\n    tel = db.Column(db.String(20))\n    register_date = db.Column(db.String(10))\n    user_group = db.Column(db.String(50))\n    auth_method = db.Column(db.String(10))\n    beans = db.Column(db.Integer)\n\n    def __init__(self, username, password, avatar=\"default.png\", nickname = \"\", description = \"\", status = \"init\",\n                    e_mail = \"\" , student_number = \"\", department = \"\", truename = \"\", tel=\"\", date = None, usergroup = \"primary\"\n                , auth_method = \"local\"):\n        # using sha512\n        #if (len(password) <= 6):\n        #    self = None\n        #    return None\n        self.username = username\n        self.password = password\n        self.avatar = avatar\n        self.nickname = nickname\n        self.description = description\n        self.status = status\n        self.e_mail = e_mail\n        self.student_number = student_number\n        self.department = department\n        self.truename = truename\n        self.tel = tel\n        self.beans = 150\n        if (date != None):\n            self.register_date = date\n        else:\n            self.register_date = datetime.now()\n        self.user_group = usergroup\n        self.auth_method = auth_method\n\n    def __repr__(self):\n        return '<User %r>' % (self.username)\n\n    #token will expire after 3600s\n    def generate_auth_token(self, expiration = 3600):\n        s = Serializer(app.config['SECRET_KEY'], expires_in = expiration)\n        str = s.dumps({'id': self.id})\n        return b64encode(str).decode('utf-8')\n\n    @staticmethod\n    def verify_auth_token(token):\n        s = Serializer(app.config['SECRET_KEY'])\n        try:\n            data = s.loads(b64decode(token))\n        except SignatureExpired:\n            return None # valid token, but expired\n        except BadSignature:\n            return None # invalid token\n        user = User.query.get(data['id'])\n        return user\n\nclass UserGroup(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(50))\n    cpu = db.Column(db.String(10))\n    memory = db.Column(db.String(10))\n    imageQuantity = db.Column(db.String(10))\n    lifeCycle = db.Column(db.String(10))\n\n    def __init__(self, name):\n        self.name = name\n        self.cpu = '100000'\n        self.memory = '2000'\n        self.imageQuantity = '10'\n        self.lifeCycle = '24'\n\n    def __repr__(self):\n        return '<UserGroup %r>' % self.name\n\nclass UserUsage(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(50))\n    cpu = db.Column(db.String(10))\n    memory = db.Column(db.String(10))\n    disk = db.Column(db.String(10))\n\n    def __init__(self, name):\n        self.username = name\n        self.cpu = '0'\n        self.memory = '0'\n        self.disk = '0'\n\n    def __repr__(self):\n        return '<UserUsage %s>cpu:%s memory:%s disk:%s' % (self.username,self.cpu,self.memory,self.disk)\n\nclass Notification(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    title = db.Column(db.String(100))\n    content = db.Column(db.String(8000))\n    create_date = db.Column(db.String(10))\n    # Status: 'open' -> Open to user, 'closed' -> Closed to user\n    status = db.Column(db.String(20))\n\n    def __init__(self, title, content=''):\n        self.title = title\n        self.content = content\n        self.create_date = datetime.utcnow()\n        self.status = 'open'\n\n    def __repr__(self):\n        return '<Notification %r>' % self.title\n\n\nclass NotificationGroups(db.Model):\n    # __tablename__ = 'notification_groups'\n    id = db.Column(db.Integer, primary_key=True)\n    notification_id = db.Column(db.Integer)\n    group_name = db.Column(db.String(100))\n\n    def __init__(self, notification_id, group_name):\n        self.notification_id = notification_id\n        self.group_name = group_name\n\n    def __repr__(self):\n        return '<Notification: %r, Group: %r>' % (self.notification_id, self.group_name)\n\nclass UserNotificationPair(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    userName = db.Column(db.String(10))\n    notifyId = db.Column(db.Integer)\n    isRead = db.Column(db.Integer)\n\n    def __init__(self, username, notifyid):\n        self.userName = username\n        self.notifyId = notifyid\n        self.isRead = 0\n\n    def __repr__(self):\n        return '<UserName: %r, NotifyId: %r>' % (self.userName, self.notifyId)\n\nclass LoginMsg(db.Model):\n    __bind_key__ = 'login'\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(10))\n    userip = db.Column(db.String(20))\n    time = db.Column(db.DateTime)\n\n    def __init__(self, username, userip):\n        self.username = username\n        self.userip = userip\n        self.time = datetime.now()\n\n    def __repr__(self):\n        return '<id=%d, username=%s, userip=%s, time=%s>' % (self.id,self.username,self.userip,self.time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n\nclass LoginFailMsg(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(10), unique=True)\n    failcnt = db.Column(db.Integer)\n    bantime = db.Column(db.DateTime)\n\n    def __init__(self, username):\n        self.username = username\n        self.failcnt = 0\n        self.bantime = datetime.now()\n\n    def __repr__(self):\n        return '<id=%d, username=%s, failcnt=%d, bantime=%s>' % (self.id,self.username,self.failcnt,self.bantime.strftime(\"%Y-%m-%d %H:%M:%S\"))\n\nclass VNode(db.Model):\n    __bind_key__ = 'history'\n    name = db.Column(db.String(100), primary_key=True)\n    laststopcpuval = db.Column(db.Float)\n    laststopruntime = db.Column(db.Integer)\n    billing = db.Column(db.Integer)\n    histories = db.relationship('History', backref='v_node', lazy='dynamic')\n\n    def __init__(self, vnode_name):\n        self.name = vnode_name\n        self.laststopcpuval = 0\n        self.billing = 0\n        self.laststopruntime = 0\n\n    def __repr__(self):\n        return '<Vnodes %s>' % (self.name)\n\nclass History(db.Model):\n    __bind_key__ = 'history'\n    id = db.Column(db.Integer, primary_key=True)\n    vnode = db.Column(db.String(100), db.ForeignKey('v_node.name'))\n    action = db.Column(db.String(30))\n    runningtime = db.Column(db.Integer)\n    cputime = db.Column(db.Float)\n    billing = db.Column(db.Integer)\n    actionTime = db.Column(db.DateTime)\n\n    def __init__(self, action, runningtime, cputime, billing):\n        self.action = action\n        self.runningtime = runningtime\n        self.cputime = cputime\n        self.billing = billing\n        self.actionTime = datetime.now()\n\n    def __repr__(self):\n        return \"{\\\"id\\\":\\\"%d\\\",\\\"vnode\\\":\\\"%s\\\",\\\"action\\\":\\\"%s\\\",\\\"runningtime\\\":\\\"%d\\\",\\\"cputime\\\":\\\"%f\\\",\\\"billing\\\":\\\"%d\\\",\\\"actionTime\\\":\\\"%s\\\"}\" % (self.id, self.vnode, self.action, self.runningtime, self.cputime, self.billing, self.actionTime.strftime(\"%Y-%m-%d %H:%M:%S\"))\n\nclass ApplyMsg(db.Model):\n    __bind_key__ = 'beansapplication'\n    id = db.Column(db.Integer, primary_key=True)\n    username = db.Column(db.String(10))\n    number = db.Column(db.Integer)\n    reason = db.Column(db.String(600))\n    status = db.Column(db.String(10))\n    time = db.Column(db.DateTime(10))\n\n    def __init__(self,username, number, reason):\n        self.username = username\n        self.number = number\n        self.reason = reason\n        self.status = \"Processing\"\n        self.time = datetime.now()\n\n    def ch2dict(self):\n        ans = {}\n        ans['id'] = self.id\n        ans['username'] = self.username\n        ans['number'] = self.number\n        ans['reason'] = self.reason\n        ans['status'] = self.status\n        ans['time'] = self.time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        return ans\n\n    def __repr__(self):\n        return \"{\\\"id\\\":\\\"%d\\\", \\\"username\\\":\\\"%s\\\", \\\"number\\\": \\\"%d\\\", \\\"reason\\\":\\\"%s\\\", \\\"status\\\":\\\"%s\\\", \\\"time\\\":\\\"%s\\\"}\" % (self.id, self.username, self.number, self.reason, self.status, self.time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n\nclass Container(db.Model):\n    __bind_key__ = 'system'\n    containername = db.Column(db.String(100), primary_key=True)\n    hostname = db.Column(db.String(30))\n    ip = db.Column(db.String(20))\n    host = db.Column(db.String(20))\n    image = db.Column(db.String(50))\n    lastsave = db.Column(db.DateTime)\n    setting_cpu = db.Column(db.Integer)\n    setting_mem = db.Column(db.Integer)\n    setting_disk = db.Column(db.Integer)\n    vclusterid = db.Column(db.Integer, db.ForeignKey('v_cluster.clusterid'))\n\n    def __init__(self, containername, hostname, ip, host, image, lastsave, setting):\n        self.containername = containername\n        self.hostname = hostname\n        self.ip = ip\n        self.host = host\n        self.image = image\n        self.lastsave = lastsave\n        self.setting_cpu = int(setting['cpu'])\n        self.setting_mem = int(setting['memory'])\n        self.setting_disk = int(setting['disk'])\n\n    def __repr__(self):\n        return \"{\\\"containername\\\":\\\"%s\\\", \\\"hostname\\\":\\\"%s\\\", \\\"ip\\\": \\\"%s\\\", \\\"host\\\":\\\"%s\\\", \\\"image\\\":\\\"%s\\\", \\\"lastsave\\\":\\\"%s\\\", \\\"setting\\\":{\\\"cpu\\\":\\\"%d\\\",\\\"memory\\\":\\\"%d\\\",\\\"disk\\\":\\\"%d\\\"}}\" % (self.containername, self.hostname, self.ip, self.host, self.image, self.lastsave.strftime(\"%Y-%m-%d %H:%M:%S\"), self.setting_cpu, self.setting_mem, self.setting_disk)\n\nclass PortMapping(db.Model):\n    __bind_key__ = 'system'\n    id = db.Column(db.Integer, primary_key=True, autoincrement=True)\n    node_name = db.Column(db.String(100))\n    node_ip = db.Column(db.String(20))\n    node_port = db.Column(db.Integer)\n    host_port= db.Column(db.Integer)\n    vclusterid = db.Column(db.Integer, db.ForeignKey('v_cluster.clusterid'))\n\n    def __init__(self, node_name, node_ip, node_port, host_port):\n        self.node_name = node_name\n        self.node_ip = node_ip\n        self.node_port = int(node_port)\n        self.host_port = int(host_port)\n\n    def __repr__(self):\n        return \"{\\\"id\\\":\\\"%d\\\", \\\"node_name\\\":\\\"%s\\\", \\\"node_ip\\\": \\\"%s\\\", \\\"node_port\\\":\\\"%s\\\", \\\"host_port\\\":\\\"%s\\\"}\" % (self.id, self.node_name, self.node_ip, self.node_port, self.host_port)\n\nclass BillingHistory(db.Model):\n    __bind_key__ = 'system'\n    node_name = db.Column(db.String(100), primary_key=True)\n    vclusterid = db.Column(db.Integer, db.ForeignKey('v_cluster.clusterid'))\n    cpu = db.Column(db.Float)\n    mem = db.Column(db.Float)\n    disk = db.Column(db.Float)\n    port = db.Column(db.Float)\n\n    def __init__(self,node_name,cpu,mem,disk,port):\n        self.node_name = node_name\n        self.cpu = cpu\n        self.mem = mem\n        self.disk = disk\n        self.port = port\n\n    def __repr__(self):\n        return \"{\\\"node_name\\\":\\\"%s\\\", \\\"cpu\\\": %f, \\\"mem\\\": %f, \\\"disk\\\": %f, \\\"port\\\": %f}\" % (self.node_name, self.cpu, self.mem, self.disk, self.port)\n\n\nclass VCluster(db.Model):\n    __bind_key__ = 'system'\n    clusterid = db.Column(db.BigInteger, primary_key=True, autoincrement=False)\n    clustername = db.Column(db.String(50))\n    ownername = db.Column(db.String(20))\n    status = db.Column(db.String(10))\n    size = db.Column(db.Integer)\n    containers = db.relationship('Container', backref='v_cluster', lazy='dynamic')\n    nextcid = db.Column(db.Integer)\n    create_time = db.Column(db.DateTime)\n    start_time = db.Column(db.String(20))\n    stop_time = db.Column(db.DateTime)\n    is_warned = db.Column(db.Boolean)\n    proxy_server_ip = db.Column(db.String(20))\n    proxy_public_ip = db.Column(db.String(20))\n    port_mapping = db.relationship('PortMapping', backref='v_cluster', lazy='dynamic')\n    billing_history = db.relationship('BillingHistory', backref='v_cluster', lazy='dynamic')\n\n    def __init__(self, clusterid, clustername, ownername, status, size, nextcid, proxy_server_ip, proxy_public_ip):\n        self.clusterid = clusterid\n        self.clustername = clustername\n        self.ownername = ownername\n        self.status = status\n        self.size = size\n        self.nextcid = nextcid\n        self.proxy_server_ip = proxy_server_ip\n        self.proxy_public_ip = proxy_public_ip\n        self.containers = []\n        self.port_mapping = []\n        self.billing_history = []\n        self.create_time = datetime.now()\n        self.start_time = \"------\"\n        self.stop_time = datetime.now()\n        self.is_warned = False\n\n    def __repr__(self):\n        info = {}\n        info[\"clusterid\"] = self.clusterid\n        info[\"clustername\"] = self.clustername\n        info[\"ownername\"] = self.ownername\n        info[\"status\"] = self.status\n        info[\"size\"] = self.size\n        info[\"proxy_server_ip\"] = self.proxy_server_ip\n        info[\"proxy_public_ip\"] = self.proxy_public_ip\n        info[\"nextcid\"] = self.nextcid\n        info[\"create_time\"] = self.create_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        info[\"start_time\"] = self.start_time\n        if self.stop_time is None:\n            info['stop_time'] = \"------\"\n        else:\n            info['stop_time'] = self.stop_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        info[\"is_warned\"] = self.is_warned\n        info[\"containers\"] = [dict(eval(str(con))) for con in self.containers]\n        info[\"port_mapping\"] = [dict(eval(str(pm))) for pm in self.port_mapping]\n        info[\"billing_history\"] = [dict(eval(str(bh))) for bh in self.billing_history]\n        #return \"{\\\"clusterid\\\":\\\"%d\\\", \\\"clustername\\\":\\\"%s\\\", \\\"ownername\\\": \\\"%s\\\", \\\"status\\\":\\\"%s\\\", \\\"size\\\":\\\"%d\\\", \\\"proxy_server_ip\\\":\\\"%s\\\", \\\"create_time\\\":\\\"%s\\\"}\" % (self.clusterid, self.clustername, self.ownername, self.status, self.size, self.proxy_server_ip, self.create_time.strftime(\"%Y-%m-%d %H:%M:%S\"))\n        return json.dumps(info)\n\nclass Image(db.Model):\n    __bind_key__ = 'system'\n    imagename = db.Column(db.String(50))\n    id = db.Column(db.Integer, primary_key=True)\n    hasPrivate = db.Column(db.Boolean)\n    hasPublic = db.Column(db.Boolean)\n    ownername = db.Column(db.String(20))\n    create_time = db.Column(db.DateTime)\n    description = db.Column(db.Text)\n\n    def __init__(self,imagename,hasPrivate,hasPublic,ownername,description):\n        self.imagename = imagename\n        self.hasPrivate = hasPrivate\n        self.hasPublic = hasPublic\n        self.ownername = ownername\n        self.description = description\n        self.create_time = datetime.now()\n\n    def __repr__(self):\n        return \"{\\\"id\\\":\\\"%d\\\",\\\"imagename\\\":\\\"%s\\\",\\\"hasPrivate\\\":\\\"%s\\\",\\\"hasPublic\\\":\\\"%s\\\",\\\"ownername\\\":\\\"%s\\\",\\\"updatetime\\\":\\\"%s\\\",\\\"description\\\":\\\"%s\\\"}\" % (self.id,self.imagename,str(self.hasPrivate),str(self.hasPublic),self.create_time.strftime(\"%Y-%m-%d %H:%M:%S\"),self.ownername,self.description)\n\nclass Batchjob(db.Model):\n    __bind_key__ = 'batch'\n    id = db.Column(db.String(9), primary_key=True)\n    username = db.Column(db.String(10))\n    name = db.Column(db.String(30))\n    priority = db.Column(db.Integer)\n    status = db.Column(db.String(10))\n    failed_reason = db.Column(db.Text)\n    create_time = db.Column(db.DateTime)\n    end_time = db.Column(db.DateTime)\n    billing = db.Column(db.Integer)\n    tasks = db.relationship('Batchtask', backref='batchjob', lazy='dynamic')\n\n    def __init__(self,id,username,name,priority):\n        self.id = id\n        self.username = username\n        self.name = name\n        self.priority = priority\n        self.status = \"pending\"\n        self.failed_reason = \"\"\n        self.create_time = datetime.now()\n        self.end_time = None\n        self.billing = 0\n\n    def clear(self):\n        self.status = \"pending\"\n        self.failed_reason = \"\"\n        self.end_time = None\n        self.billing = 0\n\n    def __repr__(self):\n        info = {}\n        info['job_id'] = self.id\n        info['username'] = self.username\n        info['job_name'] = self.name\n        info['priority'] = self.priority\n        info['status'] = self.status\n        info['failed_reason'] = self.failed_reason\n        info['create_time'] = self.create_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        if self.end_time is None:\n            info['end_time'] = \"------\"\n        else:\n            info['end_time'] = self.end_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        info['billing'] = self.billing\n        return json.dumps(info)\n\nclass Batchtask(db.Model):\n    __bind_key__ = 'batch'\n    id = db.Column(db.String(15), primary_key=True)\n    idx = db.Column(db.String(10))\n    jobid = db.Column(db.String(9), db.ForeignKey('batchjob.id'))\n    status = db.Column(db.String(15))\n    failed_reason = db.Column(db.Text)\n    start_time = db.Column(db.DateTime)\n    end_time = db.Column(db.DateTime)\n    running_time = db.Column(db.Integer)\n    billing = db.Column(db.Integer)\n    config = db.Column(db.Text)\n    tried_times = db.Column(db.Integer)\n\n    def __init__(self, id, idx, config):\n        self.id = id\n        self.idx = idx\n        self.status = \"pending\"\n        self.failed_reason = \"\"\n        self.start_time = None\n        self.end_time = None\n        self.running_time = 0\n        self.billing = 0\n        self.config = json.dumps(config)\n        self.tried_times = 0\n\n    def clear(self):\n        self.status = \"pending\"\n        self.failed_reason = \"\"\n        self.start_time = None\n        self.end_time = None\n        self.running_time = 0\n        self.billing = 0\n        self.tried_times = 0\n\n    def __repr__(self):\n        info = {}\n        info['id'] = self.id\n        info['idx'] = self.idx\n        info['jobid'] = self.jobid\n        info['status'] = self.status\n        info['failed_reason'] = self.failed_reason\n        if self.start_time is None:\n            info['start_time'] = \"------\"\n        else:\n            info['start_time'] = self.start_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        if self.end_time is None:\n            info['end_time'] = \"------\"\n        else:\n            info['end_time'] = self.end_time.strftime(\"%Y-%m-%d %H:%M:%S\")\n        info['running_time'] = self.running_time\n        info['billing'] = self.billing\n        info['config'] = json.loads(self.config)\n        info['tried_times'] = self.tried_times\n        return json.dumps(info)\n"
  },
  {
    "path": "src/utils/nettools.py",
    "content": "#!/usr/bin/python3\n\nimport subprocess, threading\nfrom utils.log import logger\nfrom utils import env\n\nclass ipcontrol(object):\n    @staticmethod\n    def parse(cmdout):\n        links = {}\n        thislink = None\n        for line in cmdout.splitlines():\n            # empty line\n            if len(line)==0:\n                continue\n            # Level 1 : first line of one link\n            if line[0] != ' ':\n                blocks = line.split()\n                thislink = blocks[1].strip(':')\n                links[thislink] = {}\n                links[thislink]['state'] = blocks[blocks.index('state')+1] if 'state' in blocks else 'UNKNOWN'\n            # Level 2 : line with 4 spaces\n            elif line[4] != ' ':\n                blocks = line.split()\n                if blocks[0] == 'inet':\n                    if 'inet' not in links[thislink]:\n                        links[thislink]['inet'] = []\n                    links[thislink]['inet'].append(blocks[1])\n                # we just need inet (IPv4)\n                else:\n                    pass\n            # Level 3 or more : no need for us\n            else:\n                pass\n        return links\n\n    @staticmethod\n    def list_links():\n        try:\n            ret = subprocess.run(['ip', 'link', 'show'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            links = ipcontrol.parse(ret.stdout.decode('utf-8'))\n            return [True, list(links.keys())]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"list links failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def link_exist(linkname):\n        try:\n            subprocess.run(['ip', 'link', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return True\n        except subprocess.CalledProcessError:\n            return False\n\n    @staticmethod\n    def link_info(linkname):\n        try:\n            ret = subprocess.run(['ip', 'address', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, ipcontrol.parse(ret.stdout.decode('utf-8'))[str(linkname)]]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"get link info failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def link_state(linkname):\n        try:\n            ret = subprocess.run(['ip', 'link', 'show', 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, ipcontrol.parse(ret.stdout.decode('utf-8'))[str(linkname)]['state']]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"get link state failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def link_ips(linkname):\n        [status, info] = ipcontrol.link_info(str(linkname))\n        if status:\n            if 'inet' not in info:\n                return [True, []]\n            else:\n                return [True, info['inet']]\n        else:\n            return [False, info]\n\n    @staticmethod\n    def up_link(linkname):\n        try:\n            subprocess.run(['ip', 'link', 'set', 'dev', str(linkname), 'up'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(linkname)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set link up failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def down_link(linkname):\n        try:\n            subprocess.run(['ip', 'link', 'set', 'dev', str(linkname), 'down'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(linkname)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set link down failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_addr(linkname, address):\n        try:\n            subprocess.run(['ip', 'address', 'add', address, 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(linkname)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add address failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def del_addr(linkname, address):\n        try:\n            subprocess.run(['ip', 'address', 'del', address, 'dev', str(linkname)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(linkname)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"delete address failed : %s\" % suberror.stdout.decode('utf-8')]\n\n\n# ovs-vsctl list-br\n# ovs-vsctl br-exists <Bridge>\n# ovs-vsctl add-br <Bridge>\n# ovs-vsctl del-br <Bridge>\n# ovs-vsctl list-ports <Bridge>\n# ovs-vsctl del-port <Bridge> <Port>\n# ovs-vsctl add-port <Bridge> <Port> -- set interface <Port> type=gre options:remote_ip=<RemoteIP>\n# ovs-vsctl add-port <Bridge> <Port> tag=<ID> -- set interface <Port> type=internal\n# ovs-vsctl port-to-br <Port>\n# ovs-vsctl set Port <Port> tag=<ID>\n# ovs-vsctl clear Port <Port> tag\n\nclass ovscontrol(object):\n    @staticmethod\n    def list_bridges():\n        try:\n            ret = subprocess.run(['ovs-vsctl', 'list-br'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, ret.stdout.decode('utf-8').split()]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"list bridges failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def bridge_exist(bridge):\n        try:\n            subprocess.run(['ovs-vsctl', 'br-exists', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return True\n        except subprocess.CalledProcessError:\n            return False\n\n    @staticmethod\n    def port_tobridge(port):\n        try:\n            ret = subprocess.run(['ovs-vsctl', 'port-to-br', str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, ret.stdout.decode('utf-8').strip()]\n        except subprocess.CalledProcessError as suberror:\n            return [False, suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def port_exists(port):\n        return ovscontrol.port_tobridge(port)[0]\n\n    @staticmethod\n    def add_bridge(bridge):\n        try:\n            subprocess.run(['ovs-vsctl', '--may-exist', 'add-br', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(bridge)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add bridge failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def del_bridge(bridge):\n        try:\n            subprocess.run(['ovs-vsctl', 'del-br', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(bridge)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"del bridge failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def list_ports(bridge):\n        try:\n            ret = subprocess.run(['ovs-vsctl', 'list-ports', str(bridge)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, ret.stdout.decode('utf-8').split()]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"list ports failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def del_port(bridge, port):\n        try:\n            subprocess.run(['ovs-vsctl', 'del-port', str(bridge), str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"delete port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_port(bridge, port):\n        try:\n            subprocess.run(['ovs-vsctl', '--may-exist', 'add-port', str(bridge), str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_port_internal(bridge, port):\n        try:\n            subprocess.run(['ovs-vsctl', '--may-exist', 'add-port', str(bridge), str(port), '--', 'set', 'interface', str(port), 'type=internal'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_port_internal_withtag(bridge, port, tag):\n        try:\n            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)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_port_gre(bridge, port, remote):\n        try:\n            subprocess.run(['ovs-vsctl', '--may-exist', '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)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def add_port_gre_withkey(bridge, port, remote, key):\n        try:\n            subprocess.run(['ovs-vsctl', '--may-exist', 'add-port', str(bridge), str(port), '--', 'set', 'interface', str(port), 'type=gre', 'options:remote_ip='+str(remote), 'options:key='+str(key)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"add port failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def set_port_tag(port, tag):\n        try:\n            subprocess.run(['ovs-vsctl', 'set', 'Port', str(port), 'tag='+str(tag)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set port tag failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def set_port_input_qos(port, input_rate_limit):\n        input_rate_limiting = int(input_rate_limit)*1000\n        if input_rate_limiting == 0:\n            return [True, str(port)]\n        try:\n            p = subprocess.run(['ovs-vsctl', 'create', 'qos', 'type=linux-htb', 'other_config:max-rate='+str(input_rate_limiting)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            subprocess.run(['ovs-vsctl', 'set', 'Port', str(port), 'qos='+p.stdout.decode('utf-8').rstrip()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set port input qos failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def del_port_input_qos(port):\n        try:\n            p = subprocess.run(['ovs-vsctl', 'get', 'port', str(port), 'qos'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            subprocess.run(['ovs-vsctl', 'clear', 'port', str(port), 'qos'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            subprocess.run(['ovs-vsctl', 'destroy', 'qos', p.stdout.decode('utf-8').rstrip()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"del port input qos failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def set_port_output_qos(port, output_rate_limit):\n        try:\n            subprocess.run(['ovs-vsctl', 'set', 'interface', str(port), 'ingress_policing_rate='+str(output_rate_limit)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            subprocess.run(['ovs-vsctl', 'set', 'interface', str(port), 'ingress_policing_burst='+str(output_rate_limit)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set port output qos failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def del_port_output_qos(port):\n        try:\n            subprocess.run(['ovs-vsctl', 'set', 'interface', str(port), 'ingress_policing_rate=0'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            subprocess.run(['ovs-vsctl', 'set', 'interface', str(port), 'ingress_policing_burst=0'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"del port output qos failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def destroy_all_qos():\n        try:\n            ret = subprocess.run(['ovs-vsctl', '--all', 'destroy', 'qos'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, 'succeed to destroying all qos.']\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"destroy all qos failed : %s\" % suberror.stdout.decode('utf-8')]\n\nclass netcontrol(object):\n    @staticmethod\n    def bridge_exists(bridge):\n        return ovscontrol.bridge_exist(bridge)\n\n    @staticmethod\n    def del_bridge(bridge):\n        return ovscontrol.del_bridge(bridge)\n\n    @staticmethod\n    def new_bridge(bridge):\n        return ovscontrol.add_bridge(bridge)\n\n    @staticmethod\n    def gre_exists(bridge, remote):\n        # port is unique, bridge is not necessary\n        return ovscontrol.port_exists('gre-'+str(remote))\n\n    @staticmethod\n    def setup_gre(bridge, remote):\n        return ovscontrol.add_port_gre(bridge, 'gre-'+str(remote), remote)\n\n    @staticmethod\n    def gw_exists(bridge, gwport):\n        return ovscontrol.port_exists(gwport)\n\n    @staticmethod\n    def setup_gw(bridge, gwport, addr, input_rate_limit, output_rate_limit):\n        [status, result] = ovscontrol.add_port_internal(bridge, gwport)\n        if not status:\n            return [status, result]\n        [status, result] = ipcontrol.add_addr(gwport, addr)\n        if not status:\n            return [status, result]\n        [status, result] = ipcontrol.up_link(gwport)\n        if not status:\n            return [status, result]\n        [status, result] = ovscontrol.set_port_input_qos(gwport, input_rate_limit)\n        if not status:\n            return [status, result]\n        return ovscontrol.set_port_output_qos(gwport, output_rate_limit)\n\n    @staticmethod\n    def del_gw(bridge, gwport):\n        [status, result] = ovscontrol.del_port_input_qos(gwport)\n        if not status:\n            return [status, result]\n        [status, result] = ovscontrol.del_port_output_qos(gwport)\n        if not status:\n            return [status, result]\n        return ovscontrol.del_port(bridge, gwport)\n\n    @staticmethod\n    def check_gw(bridge, gwport, uid, addr, input_rate_limit, output_rate_limit):\n        ovscontrol.add_bridge(bridge)\n        if not netcontrol.gw_exists(bridge, gwport):\n            return netcontrol.setup_gw(bridge, gwport, addr, input_rate_limit, output_rate_limit)\n        [status, info] = ipcontrol.link_info(gwport)\n        if not status:\n            return [False, \"get gateway info failed\"]\n        if ('inet' not in info) or (addr not in info['inet']):\n            ipcontrol.add_addr(gwport, addr)\n        else:\n            info['inet'].remove(addr)\n            for otheraddr in info['inet']:\n                ipcontrol.del_addr(gwport, otheraddr)\n        if info['state'] == 'DOWN':\n            ipcontrol.up_link(gwport)\n        return [True, \"check gateway port %s\" % gwport]\n\n    @staticmethod\n    def recover_usernet(portname, uid, GatewayHost, isGatewayHost):\n        ovscontrol.add_bridge(\"docklet-br-\"+str(uid))\n        if not isGatewayHost:\n            [success, ports] = ovscontrol.list_ports(\"docklet-br-\"+str(uid))\n            if success:\n                for port in ports:\n                    if port.startswith(\"gre\") and (not port == (\"gre-\"+str(uid)+\"-\"+GatewayHost) ) :\n                        ovscontrol.del_port(\"docklet-br-\"+str(uid),port)\n            ovscontrol.add_port_gre_withkey(\"docklet-br-\"+str(uid), \"gre-\"+str(uid)+\"-\"+GatewayHost, GatewayHost, str(uid))\n        ovscontrol.add_port(\"docklet-br-\"+str(uid), portname)\n\nfree_ports = [False]*65536\nallocated_ports = {}\nports_lock = threading.Lock()\n\nclass portcontrol(object):\n\n    @staticmethod\n    def init_new():\n        Free_Ports_str = env.getenv(\"ALLOCATED_PORTS\")\n        global free_ports\n        #logger.info(Free_Ports_str)\n        portsranges=Free_Ports_str.split(',')\n        #logger.info(postranges)\n        for portsrange in portsranges:\n            portsrange=portsrange.strip().split('-')\n            start = int(portsrange[0])\n            end = int(portsrange[1])\n            if end < start or end > 65535 or start < 1:\n                return [False, \"Illegal port ranges.\"]\n            i = start\n            #logger.info(str(start)+\" \"+str(end))\n            while i <= end:\n                free_ports[i] = True\n                i += 1\n        #logger.info(free_ports[10001])\n        return [True,\"\"]\n\n    @staticmethod\n    def init_recovery(Free_Ports_str):\n        Free_Ports_str = env.getenv(\"ALLOCATED_PORTS\")\n        return [True,\"\"]\n\n    @staticmethod\n    def acquire_port_mapping(container_name, container_ip, container_port, host_port=None):\n        global free_ports\n        global allocated_ports\n        global ports_lock\n        ports_lock.acquire()\n        # if container_name in allocated_ports.keys():\n        #     return [False, \"This container already has a port mapping.\"]\n        if container_name not in allocated_ports.keys():\n            allocated_ports[container_name] = {}\n        elif container_port in allocated_ports[container_name].keys():\n            ports_lock.release()\n            return [False, \"This container port already has a port mapping.\"]\n        if container_name == \"\" or container_ip == \"\" or container_port == \"\":\n            ports_lock.release()\n            return [False, \"Node Name or Node IP or Node Port can't be null.\"]\n        #print(\"acquire_port_mapping1\")\n        free_port = 1\n        if host_port is not None:\n            # recover from host_port\n            free_port = int(host_port)\n        else:\n            # acquire new free port\n            while free_port <= 65535:\n                if free_ports[free_port]:\n                    break\n                free_port += 1\n            if free_port == 65536:\n                ports_lock.release()\n                return [False, \"No free ports.\"]\n        free_ports[free_port] = False\n        allocated_ports[container_name][container_port] = free_port\n        public_ip = env.getenv(\"PUBLIC_IP\")\n        ports_lock.release()\n        try:\n            subprocess.run(['iptables','-t','nat','-A','PREROUTING','-p','tcp','--dport',str(free_port),\"-j\",\"DNAT\",'--to-destination','%s:%s'%(container_ip,container_port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set port mapping failed : %s\" % suberror.stdout.decode('utf-8')]\n        try:\n            subprocess.run(['iptables','-t','nat','-A','PREROUTING','-p','udp','--dport',str(free_port),\"-j\",\"DNAT\",'--to-destination','%s:%s'%(container_ip,container_port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n            return [True, str(free_port)]\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"set port mapping failed : %s\" % suberror.stdout.decode('utf-8')]\n\n    @staticmethod\n    def release_port_mapping(container_name, container_ip, container_port):\n        global free_ports\n        global allocated_ports\n        global ports_lock\n        if container_name not in allocated_ports.keys():\n            return [False, \"This container does not have a port mapping.\"]\n        free_port = allocated_ports[container_name][container_port]\n        public_ip = env.getenv(\"PUBLIC_IP\")\n        try:\n            subprocess.run(['iptables','-t','nat','-D','PREROUTING','-p','tcp','--dport',str(free_port),\"-j\",\"DNAT\",'--to-destination','%s:%s'%(container_ip,container_port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"release port mapping failed : %s\" % suberror.stdout.decode('utf-8')]\n        try:\n            subprocess.run(['iptables','-t','nat','-D','PREROUTING','-p','udp','--dport',str(free_port),\"-j\",\"DNAT\",'--to-destination','%s:%s'%(container_ip,container_port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False, check=True)\n        except subprocess.CalledProcessError as suberror:\n            return [False, \"release port mapping failed : %s\" % suberror.stdout.decode('utf-8')]\n        ports_lock.acquire()\n        free_ports[free_port] = True\n        allocated_ports[container_name].pop(container_port)\n        ports_lock.release()\n        return [True, \"\"]\n"
  },
  {
    "path": "src/utils/proxytool.py",
    "content": "#!/usr/bin/python3\n\nimport requests, json\nfrom utils import env\n\nproxy_api_port = env.getenv(\"PROXY_API_PORT\")\nproxy_control=\"http://localhost:\"+ str(proxy_api_port) +\"/api/routes\"\n\ndef get_routes():\n    try:\n        resp = requests.get(proxy_control)\n    except:\n        return [False, 'Connect Failed']\n    return [True, resp.json()]\n\ndef set_route(path, target):\n    path='/'+path.strip('/')\n    if path=='' or target=='':\n        return [False, 'input not valid']\n    try:\n        resp = requests.post(proxy_control+path, data=json.dumps({'target':target}))\n    except:\n        return [False, 'Connect Failed']\n    return [True, 'set ok']\n\ndef delete_route(path):\n    path='/'+path.strip('/')\n    try:\n        resp = requests.delete(proxy_control+path)\n    except:\n        return [False, 'Connect Failed']\n    # if exist and delete, status_code=204, if not exist, status_code=404\n    return [True, 'delete ok']\n"
  },
  {
    "path": "src/utils/tools.py",
    "content": "#!/usr/bin/python3\n\nimport os, random\n\n#from log import logger\n\ndef loadenv(configpath):\n    configfile = open(configpath)\n    #logger.info (\"load environment from %s\" % configpath)\n    for line in configfile:\n        line = line.strip()\n        if line == '':\n            continue\n        keyvalue = line.split(\"=\")\n        if len(keyvalue) < 2:\n            continue\n        key = keyvalue[0].strip()\n        value = keyvalue[1].strip()\n        #logger.info (\"load env and put env %s:%s\" % (key, value))\n        os.environ[key] = value\n        \ndef gen_token():\n    return str(random.randint(10000, 99999))+\"-\"+str(random.randint(10000, 99999))\n"
  },
  {
    "path": "src/utils/updatebase.py",
    "content": "#!/usr/bin/python3\n\nimport os, shutil\nfrom utils.log import logger\n\ndef aufs_remove(basefs):\n    try:\n        if os.path.isdir(basefs):\n            shutil.rmtree(basefs)\n        elif os.path.isfile(basefs):\n            os.remove(basefs)\n    except Exception as e:\n        logger.error(e)\n\ndef aufs_clean(basefs):\n    # clean the aufs mark\n    allfiles = os.listdir(basefs)\n    for onefile in allfiles:\n        if onefile[:4] == \".wh.\":\n            aufs_remove(basefs + \"/\" + onefile)\n\ndef aufs_merge(image, basefs):\n    allfiles = os.listdir(image)\n    if \".wh..wh..opq\" in allfiles:\n        #this is a new dir in image, remove the dir in basefs with the same name, and copy it to basefs\n        shutil.rmtree(basefs)\n        shutil.copytree(image, basefs, symlinks=True)\n        aufs_clean(basefs)\n        return\n    for onefile in allfiles:\n        try:\n            if onefile[:7] == \".wh..wh\":\n                # aufs mark, but not white-out mark, ignore it\n                continue\n            elif onefile[:4] == \".wh.\":\n                # white-out mark, remove the file in basefs\n                aufs_remove(basefs + \"/\" + onefile[4:])\n            elif os.path.isdir(image + \"/\" + onefile):\n                if os.path.isdir(basefs + \"/\" + onefile):\n                    # this is a dir in image and basefs, merge it\n                    aufs_merge(image + \"/\" + onefile, basefs + \"/\" + onefile)\n                elif os.path.isfile(basefs + \"/\" + onefile):\n                    # this is a dir in image but file in basefs, remove the file and copy the dir to basefs\n                    os.remove(basefs + \"/\" + onefile)\n                    shutil.copytree(image + \"/\" + onefile, basefs + \"/\" + onefile, symlinks=True)\n                elif not os.path.exists(basefs + \"/\" + onefile):\n                    # this is a dir in image but not exists in basefs, copy the dir to basefs\n                    shutil.copytree(image + \"/\" + onefile, basefs + \"/\" + onefile, symlinks=True)\n                else:\n                    # error\n                    logger.error(basefs + \"/\" + onefile + \" cause error\")\n            elif os.path.isfile(image + \"/\" + onefile):\n                if os.path.isdir(basefs + \"/\" + onefile):\n                    # this is a file in image but dir in basefs, remove the dir and copy the file to basefs\n                    shutil.rmtree(basefs + \"/\" + onefile)\n                    shutil.copy2(image+ \"/\" + onefile, basefs + \"/\" + onefile, follow_symlinks=False)\n                elif os.path.isfile(basefs + \"/\" + onefile):\n                    # this is a file in image and basefs, remove the file and copy the file to basefs\n                    os.remove(basefs + \"/\" + onefile)\n                    shutil.copy2(image+ \"/\" + onefile, basefs + \"/\" + onefile, follow_symlinks=False)\n                elif not os.path.isdir(basefs + \"/\" + onefile):\n                    # this is a file in image but not exists in basefs, copy the file to basefs\n                    shutil.copy2(image+ \"/\" + onefile, basefs + \"/\" + onefile, follow_symlinks=False)\n                else:\n                    # error\n                    logger.error(basefs + \"/\" + onefile + \" cause error\")\n        except Exception as e:\n            logger.error(e)\n\ndef aufs_update_base(image, basefs):\n    if not os.path.isdir(basefs):\n        logger.error(\"basefs:%s doesn't exists\" % basefs)\n    if not os.path.isdir(image):\n        logger.error(\"image:%s doesn't exists\" % image)\n    aufs_merge(image, basefs)\n"
  },
  {
    "path": "src/worker/container.py",
    "content": "#!/usr/bin/python3\n\nimport subprocess, os, json, traceback\nfrom utils.log import logger\nfrom utils import env, imagemgr\nfrom utils.lvmtool import sys_run, check_volume\nfrom worker.monitor import Container_Collector, History_Manager\nimport lxc\nfrom utils import model\n\nclass Container(object):\n    def __init__(self, addr, etcdclient):\n        self.addr = addr\n        self.etcd=etcdclient\n        self.libpath = env.getenv('DOCKLET_LIB')\n        self.confpath = env.getenv('DOCKLET_CONF')\n        self.fspath = env.getenv('FS_PREFIX')\n        # set jupyter running dir in container\n        self.rundir = \"/home/jupyter\"\n        # set root running dir in container\n        self.nodehome = \"/root\"\n\n        self.lxcpath = \"/var/lib/lxc\"\n        self.imgmgr = imagemgr.ImageMgr()\n        self.historymgr = History_Manager()\n\n    def prepare_hook_conf(self, conf_path, env_dict):\n        try:\n            confile = open(conf_path, \"w\")\n            for k,v in env_dict.items():\n                confile.write(\"%s=%s\\n\"%(k,v))\n            confile.close()\n        except Exception as e:\n            logger.error(traceback.format_exc())\n            return [False, e]\n        return [True, \"\"]\n\n    def create_container(self, lxc_name, proxy_server_ip, username, uid, setting, clustername, clusterid, containerid, hostname, ip, gateway, image):\n        logger.info(\"create container %s of %s for %s\" %(lxc_name, clustername, username))\n        try:\n            setting = json.loads(setting)\n            cpu = int(setting['cpu']) * 100000\n            memory = setting[\"memory\"]\n            disk = setting[\"disk\"]\n            image = json.loads(image)\n            status = self.imgmgr.prepareFS(username,image,lxc_name,disk)\n            if not status:\n                return [False, \"Create container failed when preparing filesystem, possibly insufficient space\"]\n\n            #Ret = subprocess.run([self.libpath+\"/lxc_control.sh\",\n            #    \"create\", lxc_name, username, str(clusterid), hostname,\n            #    ip, gateway, str(cpu), str(memory)], stdout=subprocess.PIPE,\n            #    stderr=subprocess.STDOUT,shell=False, check=True)\n\n            rootfs = \"/var/lib/lxc/%s/rootfs\" % lxc_name\n\n            if not os.path.isdir(\"%s/global/users/%s\" % (self.fspath,username)):\n                path = env.getenv('DOCKLET_LIB')\n                subprocess.call([path+\"/master/userinit.sh\", username])\n                logger.info(\"user %s directory not found, create it\" % username)\n            sys_run(\"mkdir -p /var/lib/lxc/%s\" % lxc_name)\n            logger.info(\"generate config file for %s\" % lxc_name)\n\n            def config_prepare(content):\n                content = content.replace(\"%ROOTFS%\",rootfs)\n                content = content.replace(\"%HOSTNAME%\",hostname)\n                content = content.replace(\"%IP%\",ip)\n                content = content.replace(\"%GATEWAY%\",gateway)\n                content = content.replace(\"%CONTAINER_MEMORY%\",str(memory))\n                content = content.replace(\"%CONTAINER_CPU%\",str(cpu))\n                content = content.replace(\"%FS_PREFIX%\",self.fspath)\n                content = content.replace(\"%USERNAME%\",username)\n                content = content.replace(\"%CLUSTERID%\",str(clusterid))\n                content = content.replace(\"%LXCSCRIPT%\",env.getenv(\"LXC_SCRIPT\"))\n                content = content.replace(\"%LXCNAME%\",lxc_name)\n                content = content.replace(\"%UserID%\",str(uid))\n                content = content.replace(\"%CLUSTERNAME%\", clustername)\n                content = content.replace(\"%VETHPAIR%\", str(clusterid)+'-'+str(containerid))\n                return content\n\n            conffile = open(self.confpath+\"/container.conf\", 'r')\n            conftext = conffile.read()\n            conffile.close()\n            conftext = config_prepare(conftext)\n\n            conffile = open(\"/var/lib/lxc/%s/config\" % lxc_name,\"w\")\n            conffile.write(conftext)\n            conffile.close()\n\n            if os.path.isfile(self.confpath+\"/lxc.custom.conf\"):\n                conffile = open(self.confpath+\"/lxc.custom.conf\", 'r')\n                conftext = conffile.read()\n                conffile.close()\n                conftext = config_prepare(conftext)\n                conffile = open(\"/var/lib/lxc/%s/config\" % lxc_name, 'a')\n                conffile.write(conftext)\n                conffile.close()\n\n            hook_env = {}\n            hook_env['Bridge'] = \"docklet-br-%d\" % uid\n            hook_env['HNAME'] = hostname\n            self.prepare_hook_conf(rootfs+\"/../env.conf\",hook_env)\n\n            #logger.debug(Ret.stdout.decode('utf-8'))\n            logger.info(\"create container %s success\" % lxc_name)\n\n            # get AUTH COOKIE URL for jupyter\n            [status, authurl] = self.etcd.getkey(\"web/authurl\")\n            if not status:\n                [status, masterip] = self.etcd.getkey(\"service/master\")\n                if status:\n                    webport = env.getenv(\"WEB_PORT\")\n                    authurl = \"http://%s:%s/jupyter\" % (masterip,\n                            webport)\n                else:\n                    logger.error (\"get AUTH COOKIE URL failed for jupyter\")\n                    authurl = \"error\"\n\n            cookiename='docklet-jupyter-cookie'\n\n            rundir = self.lxcpath+'/'+lxc_name+'/rootfs' + self.rundir\n\n            logger.debug(rundir)\n\n            if not os.path.exists(rundir):\n                os.makedirs(rundir)\n            else:\n                if not os.path.isdir(rundir):\n                    os.remove(rundir)\n                    os.makedirs(rundir)\n\n            jconfigpath = rundir + '/jupyter.config'\n            config = open(jconfigpath, 'w')\n            jconfigs=\"\"\"USER=%s\nPORT=%d\nCOOKIE_NAME=%s\nBASE_URL=%s\nHUB_PREFIX=%s\nHUB_API_URL=%s\nIP=%s\n\"\"\" % (username, 10000, cookiename, '/'+ proxy_server_ip +'/go/'+username+'/'+clustername, '/jupyter',\n        authurl, ip.split('/')[0])\n            config.write(jconfigs)\n            config.close()\n\n        except subprocess.CalledProcessError as sube:\n            logger.error('create container %s failed: %s' % (lxc_name,\n                    sube.stdout.decode('utf-8')))\n            return [False, \"create container failed\"]\n        except Exception as e:\n            logger.error(e)\n            return [False, \"create container failed\"]\n        self.historymgr.log(lxc_name,\"Create\")\n        return [True, \"create container success\"]\n\n    def delete_container(self, lxc_name):\n        logger.info (\"delete container:%s\" % lxc_name)\n        if self.imgmgr.deleteFS(lxc_name):\n            Container_Collector.billing_increment(lxc_name)\n            self.historymgr.log(lxc_name,\"Delete\")\n            logger.info(\"delete container %s success\" % lxc_name)\n            return [True, \"delete container success\"]\n        else:\n            logger.info(\"delete container %s failed\" % lxc_name)\n            return [False, \"delete container failed\"]\n        #status = subprocess.call([self.libpath+\"/lxc_control.sh\", \"delete\", lxc_name])\n        #if int(status) == 1:\n        #    logger.error(\"delete container %s failed\" % lxc_name)\n        #    return [False, \"delete container failed\"]\n        #else:\n        #    logger.info (\"delete container %s success\" % lxc_name)\n        #    return [True, \"delete container success\"]\n\n    # start container, if running, restart it\n    def start_container(self, lxc_name):\n        logger.info (\"start container:%s\" % lxc_name)\n        c = lxc.Container(lxc_name)\n        if not c.start():\n            logger.error('start container %s failed' % lxc_name)\n            return [False, \"start container failed\"]\n        else:\n            logger.info (\"start container %s success\" % lxc_name)\n            self.historymgr.log(lxc_name,\"Start\")\n            return [True, \"start container success\"]\n\n\n\n    # start container services\n    # for the master node, jupyter must be started,\n    # for other node, ssh must be started.\n    # container must be RUNNING before calling this service\n    def start_services(self, lxc_name, services=[]):\n        logger.info (\"start services for container %s: %s\" % (lxc_name, services))\n        c = lxc.Container(lxc_name)\n\n        Ret = c.attach_wait(lxc.attach_run_command,[\"service\",\"ssh\",\"start\"])\n        if Ret == 0:\n            if len(services) == 0: # master node\n                Ret = c.attach_wait(lxc.attach_run_command,[\"su\",\"-c\",\"%s/start_jupyter.sh\" % self.rundir])\n                if Ret == 0:\n                    logger.info(\"start ssh and jupyter notebook services for container %s success\" % lxc_name)\n                    return [True, \"start container services success\"]\n                else:\n                    logger.error('start services for container %s failed:jupyter' % lxc_name)\n                    return [False, \"start services for container failed:jupyter\"]\n            else:\n                logger.info(\"start ssh service for container %s success\" % lxc_name)\n                return [True, \"start container services success\"]\n        logger.error('start services for container %s failed:ssh' % lxc_name)\n        return [False, \"start services for container failed:ssh\"]\n\n    # mount_container: mount base image and user image by aufs\n    def mount_container(self,lxc_name):\n        logger.info (\"mount container:%s\" % lxc_name)\n        [success, status] = self.container_status(lxc_name)\n        if not success:\n            return [False, status]\n        self.imgmgr.checkFS(lxc_name)\n        return [True, \"mount success\"]\n\n    # recover container: if running, do nothing. if stopped, start it\n    def recover_container(self, lxc_name):\n        logger.info (\"recover container:%s\" % lxc_name)\n        #status = subprocess.call([self.libpath+\"/lxc_control.sh\", \"status\", lxc_name])\n        [success, status] = self.container_status(lxc_name)\n        if not success:\n            return [False, status]\n        self.imgmgr.checkFS(lxc_name)\n        if status == 'stopped':\n            logger.info(\"%s stopped, recover it to running\" % lxc_name)\n            if self.start_container(lxc_name)[0]:\n                self.historymgr.log(lxc_name,\"Recover\")\n                if self.start_services(lxc_name)[0]:\n                    logger.info(\"%s recover success\" % lxc_name)\n                    return [True, \"recover success\"]\n                else:\n                    logger.error(\"%s recover failed with services not start\" % lxc_name)\n                    return [False, \"recover failed for services not start\"]\n            else:\n                logger.error(\"%s recover failed for container starting failed\" % lxc_name)\n                return [False, \"recover failed for container starting failed\"]\n        else:\n            logger.info(\"%s recover success\" % lxc_name)\n            return [True, \"recover success\"]\n\n    def update_baseurl(self, lxc_name, old_ip, new_ip):\n        rundir = self.lxcpath+'/'+lxc_name+'/rootfs' + self.rundir\n        if not os.path.exists(rundir):\n            return [False, \"container %s doesn't exist\"%(lxc_name)]\n        jconfigpath = rundir + '/jupyter.config'\n        config = open(jconfigpath, 'r')\n        context = config.read()\n        config.close()\n        context = context.replace(old_ip+\"/go\", new_ip+\"/go\")\n        config = open(jconfigpath, 'w')\n        config.write(context)\n        config.close()\n        return [True,\"success\"]\n\n    def stop_container(self, lxc_name):\n        logger.info (\"stop container:%s\" % lxc_name)\n        [success, status] = self.container_status(lxc_name)\n        if not success:\n            return [False, status]\n        if status == \"running\":\n            c = lxc.Container(lxc_name)\n            if not c.stop():\n                logger.error(\"stop container %s failed\" % lxc_name)\n                return [False, \"stop container failed\"]\n            else:\n                self.historymgr.log(lxc_name,\"Stop\")\n                logger.info(\"stop container %s success\" % lxc_name)\n                return [True, \"stop container success\"]\n        else:\n            logger.info(\"container %s already stopped\" % lxc_name)\n            return [True, \"stop container success\"]\n\n    def detach_container(self, lxc_name):\n        logger.info(\"detach container:%s\" % lxc_name)\n        [success, status] = self.container_status(lxc_name)\n        if not success:\n            return [False, status]\n        if status == 'running':\n            logger.error(\"container %s is running, please stop it first\" % lxc_name)\n        self.imgmgr.detachFS(lxc_name)\n        return [True, \"detach container success\"]\n\n    # check container: check LV and mountpoints, if wrong, try to repair it\n    def check_container(self, lxc_name):\n        logger.info (\"check container:%s\" % lxc_name)\n        if not check_volume(\"docklet-group\", lxc_name):\n            logger.error(\"check container %s failed\" % lxc_name)\n            return [False, \"check container failed\"]\n        #status = subprocess.call([self.libpath+\"/lxc_control.sh\", \"check\", lxc_name])\n        self.imgmgr.checkFS(lxc_name)\n        logger.info (\"check container %s success\" % lxc_name)\n        return [True, \"check container success\"]\n\n    def is_container(self, lxc_name):\n        if lxc.Container(lxc_name).defined:\n            return True\n        else:\n            return False\n\n    def container_status(self, lxc_name):\n        if not self.is_container(lxc_name):\n            return [False, \"container not found\"]\n        c = lxc.Container(lxc_name)\n        if c.running:\n            return [True, 'running']\n        else:\n            return [True, 'stopped']\n\n    def list_containers(self):\n        lxclist = []\n        for c in lxc.list_containers(as_object=True):\n            lxclist.append(c.name)\n        return [True, lxclist]\n\n    def delete_allcontainers(self):\n        logger.info (\"deleting all containers...\")\n        [status, containers] = self.list_containers()\n        result = True\n        for container in containers:\n            [result, status] = self.container_status(container)\n            if status=='running':\n                self.stop_container(container)\n            result = result & self.delete_container(container)[0]\n        if result:\n            logger.info (\"deleted all containers success\")\n            return [True, 'all deleted']\n        else:\n            logger.error (\"deleted all containers failed\")\n            return [False, 'some containers delete failed']\n\n    # list containers in /var/lib/lxc/ as local\n    # list containers in FS_PREFIX/global/... on this host as global\n    def diff_containers(self):\n        [status, localcontainers] = self.list_containers()\n        containers = model.Container.query.all()\n        globalcontainers = []\n        for con in containers:\n            if con.host == self.addr:\n                globalcontainers.append(con.containername)\n        both = []\n        onlylocal = []\n        onlyglobal = []\n        for container in localcontainers:\n            if container in globalcontainers:\n                both.append(container)\n            else:\n                onlylocal.append(container)\n        for container in globalcontainers:\n            if container not in localcontainers:\n                onlyglobal.append(container)\n        return [both, onlylocal, onlyglobal]\n\n    def create_image(self,username,imagename,containername,description=\"not thing\",imagenum=10):\n        return self.imgmgr.createImage(username,imagename,containername,description,imagenum)\n\n    def update_basefs(self,imagename):\n        return self.imgmgr.update_basefs(imagename)\n\n    # check all local containers\n    def check_allcontainers(self):\n        [both, onlylocal, onlyglobal] = self.diff_containers()\n        logger.info(\"check all containers and repair them\")\n        status = True\n        result = True\n        for container in both:\n            logger.info (\"%s in LOCAL and GLOBAL checks...\" % container)\n            [status, meg]=self.check_container(container)\n            result = result & status\n        if len(onlylocal) > 0:\n            result = False\n            logger.error (\"some container only exists in LOCAL: %s\" % onlylocal)\n        if len(onlyglobal) > 0:\n            result = False\n            logger.error (\"some container only exists in GLOBAL: %s\" % onlyglobal)\n        if status:\n            logger.info (\"check all containers success\")\n            return [True, 'all is ok']\n        else:\n            logger.error (\"check all containers failed\")\n            return [False, 'not ok']\n"
  },
  {
    "path": "src/worker/monitor.py",
    "content": "#!/usr/bin/python3\n\n'''\nMonitor for Docklet\nDescription:Monitor system for docklet will collect data on resources usages and status of vnode\n            and phyiscal machines. And master can fetch these data and then show them on the web page.\n            Besides, Monitor will also bill the vnodes according to their resources usage amount.\n\nDesign:Monitor mainly consists of three parts: Collectors, Master_Collector and Fetchers.\n       1.Collectors will collect data every two seconds on each worker. And 'Container_Collector' will\n       collect data of containers(vnodes), while 'Collector' will collect data of physical machines.\n       2.'Master_Collector' only runs on Master. It fetches the data on workers every two seconds by rpc\n       and stores them in the memory of Master.\n       3.Fetchers are classes that Master will use them to fetch specific data in the memory and then show\n       them on the web. 'Container_Fetcher' is the class to fetch the containers data in 'monitor_vnodes',\n       while 'Fetcher' is the class to fetch the data of physical machines in 'monitor_hosts'.\n'''\n\n\nimport subprocess,re,os,psutil,math,sys\nimport time,threading,json,traceback,platform\nfrom utils import env, etcdlib, gputools\nimport lxc\nimport xmlrpc.client\nfrom datetime import datetime\n\nfrom utils.model import db,VNode,History,BillingHistory,VCluster,PortMapping\nfrom utils.log import logger\nfrom httplib2 import Http\nfrom urllib.parse import urlencode\n\n# billing parameters\na_cpu = 500         # seconds\nb_mem = 2000000     # MB\nc_disk = 4000       # MB\nd_port = 1\n\n# major dict to store the monitoring data on Worker\n# only use on Worker\n# workerinfo: only store the data collected on current Worker,\n# has the first keys same as the second keys in monitor_hosts.\nworkerinfo = {}\n\n# workercinfo: only store the data collected on current Worker,\n# use the names of vnodes(containers) as second key.\n# has the second keys same as the third keys in monitor_vnodes.\nworkercinfo = {}\n\n# store the network statistics of users' gateways on current Worker.\n# key is username\n# bytes_sent and bytes_recv are the second keys\ngateways_stats = {}\n\n# only use on worker\ncontainerpids = []\npid2name = {}\nG_masterip = \"\"\n\n# only use on worker\nlaststopcpuval = {}\nlaststopruntime = {}\nlastbillingtime = {}\n# increment has keys: lastcputime,memincrement.\n# record the cpu val at last billing time and accumulate the memory usages during this billing hour.\nincrement = {}\n\n# send http request to master\ndef request_master(url,data):\n    global G_masterip\n    header = {'Content-Type':'application/x-www-form-urlencoded'}\n    http = Http()\n    [resp,content] = http.request(\"http://\"+G_masterip+url,\"POST\",urlencode(data),headers = header)\n    logger.info(\"response from master:\"+content.decode('utf-8'))\n\n# The class is to collect data of containers on each worker\nclass Container_Collector(threading.Thread):\n\n    def __init__(self,test=False):\n        global laststopcpuval\n        global workercinfo\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.interval = 2\n        self.billingtime = 3600     # billing interval\n        self.test = test\n        self.cpu_last = {}\n        self.cpu_quota = {}\n        self.mem_quota = {}\n        self.net_stats = {}\n        self.cores_num = int(subprocess.getoutput(\"grep processor /proc/cpuinfo | wc -l\"))\n        containers = self.list_container()\n        for container in containers:    # recovery\n            if not container == '':\n                try:\n                    vnode = VNode.query.get(container)\n                    laststopcpuval[container] = vnode.laststopcpuval\n                    laststopruntime[container] = vnode.laststopruntime\n                    workercinfo[container] = {}\n                    workercinfo[container]['basic_info'] = {}\n                    workercinfo[container]['basic_info']['billing'] = vnode.billing\n                    workercinfo[container]['basic_info']['billing_history'] = get_billing_history(container)\n                    workercinfo[container]['basic_info']['RunningTime'] = vnode.laststopruntime\n                    workercinfo[container]['basic_info']['a_cpu'] = a_cpu\n                    workercinfo[container]['basic_info']['b_mem'] = b_mem\n                    workercinfo[container]['basic_info']['c_disk'] = c_disk\n                    workercinfo[container]['basic_info']['d_port'] = d_port\n                except:\n                    laststopcpuval[container] = 0\n                    laststopruntime[container] = 0\n        return\n\n    # list containers on this worker\n    def list_container(self):\n        output = subprocess.check_output([\"sudo lxc-ls\"],shell=True)\n        output = output.decode('utf-8')\n        containers = re.split('\\s+',output)\n        return containers\n\n    # get running time of a process, return seconds\n    def get_proc_etime(self,pid):\n        fmt = subprocess.getoutput(\"ps -A -opid,etime | grep '^ *%d ' | awk '{print $NF}'\" % pid).strip()\n        if fmt == '':\n            return -1\n        parts = fmt.split('-')\n        days = int(parts[0]) if len(parts) == 2 else 0\n        fmt = parts[-1]\n        parts = fmt.split(':')\n        hours = int(parts[0]) if len(parts) == 3 else 0\n        parts = parts[len(parts)-2:]\n        minutes = int(parts[0])\n        seconds = int(parts[1])\n        return ((days * 24 + hours) * 60 + minutes) * 60 + seconds\n\n    # compute the billing val this running hour\n    # if isreal is True, it will also make users' beans decrease to pay for the bill.\n    # return the billing value in this running hour\n    @classmethod\n    def billing_increment(cls,vnode_name,isreal=True):\n        global increment\n        global workercinfo\n        global G_masterip\n        global a_cpu\n        global b_mem\n        global c_disk\n        global d_port\n        cpu_val = '0'\n        if vnode_name not in workercinfo.keys():\n            return {'total': 0}\n        if 'cpu_use' in workercinfo[vnode_name].keys():\n            cpu_val = workercinfo[vnode_name]['cpu_use']['val']\n        if vnode_name not in increment.keys():\n            increment[vnode_name] = {}\n            increment[vnode_name]['lastcputime'] = cpu_val\n            increment[vnode_name]['memincrement'] = 0\n        # compute cpu used time during this running hour\n        cpu_increment = float(cpu_val) - float(increment[vnode_name]['lastcputime'])\n        #logger.info(\"billing:\"+str(cpu_increment)+\" \"+str(increment[container_name]['lastcputime']))\n        if cpu_increment == 0.0:\n            avemem = 0\n        else:\n            # avemem = (average memory used) * (cpu used time)\n            avemem = cpu_increment*float(increment[vnode_name]['memincrement'])/1800.0\n        if 'disk_use' in workercinfo[vnode_name].keys():\n            disk_quota = workercinfo[vnode_name]['disk_use']['total']\n        else:\n            disk_quota = 0\n        # get ports\n        ports_count = count_port_mapping(vnode_name)\n        # billing value = cpu used/a + memory used/b + disk quota/c + ports\n        billing = {}\n        billing['cpu'] = round(cpu_increment/a_cpu, 2)\n        billing['cpu_time'] = round(cpu_increment, 2)\n        billing['mem'] = round(avemem/b_mem, 2)\n        billing['mem_use'] = round(avemem, 2)\n        billing['disk'] = round(float(disk_quota)/1024.0/1024.0/c_disk, 2)\n        billing['disk_use'] = round(float(disk_quota)/1024.0/1024.0, 2)\n        billing['port'] = round(ports_count/d_port, 2)\n        billing['port_use'] = ports_count\n        billing['total'] = math.ceil(billing['cpu'] + billing['mem'] + billing['disk'] + billing['port'])\n        billingval = billing['total']\n        if billingval > 100:\n            # report outsize billing value\n            logger.info(\"Huge Billingval for \"+vnode_name+\". cpu_increment:\"+str(cpu_increment)+\" avemem:\"+str(avemem)+\" disk:\"+str(disk_quota)+\"\\n\")\n        if not isreal:\n            # only compute\n            return billing\n        # initialize increment for next billing\n        increment[vnode_name]['lastcputime'] = cpu_val\n        increment[vnode_name]['memincrement'] = 0\n        if 'basic_info' not in workercinfo[vnode_name].keys():\n            workercinfo[vnode_name]['basic_info'] = {}\n            workercinfo[vnode_name]['basic_info']['billing'] = 0\n            workercinfo[vnode_name]['basic_info']['RunningTime'] = 0\n        # update monitoring data\n        nowbillingval = workercinfo[vnode_name]['basic_info']['billing']\n        nowbillingval += billingval\n        workercinfo[vnode_name]['basic_info']['billing'] = nowbillingval\n        workercinfo[vnode_name]['basic_info']['billing_history'] = get_billing_history(vnode_name)\n        workercinfo[vnode_name]['basic_info']['billing_history']['cpu'] += billing['cpu']\n        workercinfo[vnode_name]['basic_info']['billing_history']['mem'] += billing['mem']\n        workercinfo[vnode_name]['basic_info']['billing_history']['disk'] += billing['disk']\n        workercinfo[vnode_name]['basic_info']['billing_history']['port'] += billing['port']\n        # update vnodes billing history\n        save_billing_history(vnode_name, workercinfo[vnode_name]['basic_info']['billing_history'])\n        # update vnodes' tables in database\n        try:\n            vnode = VNode.query.get(vnode_name)\n            vnode.billing = nowbillingval\n        except Exception as err:\n            vnode = VNode(vnode_name)\n            vnode.billing = nowbillingval\n            db.session.add(vnode)\n            logger.warning(err)\n        try:\n            db.session.commit()\n        except Exception as err:\n            db.session.rollback()\n            logger.error(traceback.format_exc())\n            logger.error(err)\n            raise\n        # update users' tables in database\n        owner_name = get_owner(vnode_name)\n        auth_key = env.getenv('AUTH_KEY')\n        data = {\"owner_name\":owner_name,\"billing\":billingval, \"auth_key\":auth_key}\n        request_master(\"/billing/beans/\",data)\n        return billing\n\n    # Collect net statistics of containers by psutil\n    def collect_net_stats(self):\n        raw_stats = psutil.net_io_counters(pernic=True)\n        for key in raw_stats.keys():\n            if re.match('[\\d]+-[\\d]+',key) is not None:\n                if key not in self.net_stats.keys():\n                    self.net_stats[key] = {}\n                    self.net_stats[key]['bytes_sent'] = 0\n                    self.net_stats[key]['bytes_recv'] = 0\n                self.net_stats[key]['bytes_recv_per_sec'] = round((int(raw_stats[key].bytes_sent) - self.net_stats[key]['bytes_recv']) / self.interval)\n                self.net_stats[key]['bytes_sent_per_sec'] = round((int(raw_stats[key].bytes_recv) - self.net_stats[key]['bytes_sent']) / self.interval)\n                self.net_stats[key]['bytes_recv'] = int(raw_stats[key].bytes_sent)\n                self.net_stats[key]['bytes_sent'] = int(raw_stats[key].bytes_recv)\n                self.net_stats[key]['packets_recv'] = int(raw_stats[key].packets_sent)\n                self.net_stats[key]['packets_sent'] = int(raw_stats[key].packets_recv)\n                self.net_stats[key]['errin'] = int(raw_stats[key].errout)\n                self.net_stats[key]['errout'] = int(raw_stats[key].errin)\n                self.net_stats[key]['dropin'] = int(raw_stats[key].dropout)\n                self.net_stats[key]['dropout'] = int(raw_stats[key].dropin)\n            else:\n                if key not in gateways_stats.keys():\n                    gateways_stats[key] = {}\n                gateways_stats[key]['bytes_recv'] = int(raw_stats[key].bytes_sent)\n                gateways_stats[key]['bytes_sent'] = int(raw_stats[key].bytes_recv)\n                gateways_stats[key]['bytes_total'] = gateways_stats[key]['bytes_recv'] + gateways_stats[key]['bytes_sent']\n        #logger.info(self.net_stats)\n\n    # the main function to collect monitoring data of a container\n    def collect_containerinfo(self,container_name):\n        global workerinfo\n        global workercinfo\n        global increment\n        global lastbillingtime\n        global containerpids\n        global pid2name\n        global laststopcpuval\n        global laststopruntime\n        is_batch = container_name.split('-')[1] == 'batch'\n        # collect basic information, such as running time,state,pid,ip,name\n        container = lxc.Container(container_name)\n        basic_info = {}\n        basic_exist = 'basic_info' in workercinfo[container_name].keys()\n        if basic_exist:\n            basic_info = workercinfo[container_name]['basic_info']\n        else:\n            basic_info['RunningTime'] = 0\n            basic_info['billing'] = 0\n        if 'billing_this_hour' not in basic_info.keys():\n            basic_info['billing_this_hour'] = {'total': 0}\n        basic_info['Name'] = container_name\n        basic_info['State'] = container.state\n        #if basic_exist:\n         #   logger.info(workercinfo[container_name]['basic_info'])\n        if(container.state == 'STOPPED'):\n            workercinfo[container_name]['basic_info'] = basic_info\n            #logger.info(basic_info)\n            return False\n        container_pid_str = str(container.init_pid)\n        if not container_pid_str in containerpids:\n            containerpids.append(container_pid_str)\n            pid2name[container_pid_str] = container_name\n        running_time = self.get_proc_etime(container.init_pid)\n        if not is_batch:\n            running_time += laststopruntime[container_name]\n        basic_info['PID'] = container_pid_str\n        basic_info['IP'] = container.get_ips()[0]\n        basic_info['RunningTime'] = running_time\n        workercinfo[container_name]['basic_info'] = basic_info\n\n        # deal with cpu used value\n        cpu_val = float(\"%.2f\" % (float(container.get_cgroup_item(\"cpuacct.usage\")) / 1000000000))\n        cpu_unit = \"seconds\"\n        if not container_name in self.cpu_last.keys():\n            # read quota from config of container\n            confpath = \"/var/lib/lxc/%s/config\"%(container_name)\n            if os.path.exists(confpath):\n                confile = open(confpath,'r')\n                res = confile.read()\n                lines = re.split('\\n',res)\n                for line in lines:\n                    words = re.split('=',line)\n                    key = words[0].strip()\n                    if key == \"lxc.cgroup.memory.limit_in_bytes\":\n                        # get memory quota, change unit to KB\n                        self.mem_quota[container_name] = float(words[1].strip().strip(\"M\"))*1000000/1024\n                    elif key == \"lxc.cgroup.cpu.cfs_quota_us\":\n                        # get cpu quota, change unit to cores\n                        tmp = int(words[1].strip())\n                        if tmp < 0:\n                            self.cpu_quota[container_name] = self.cores_num\n                        else:\n                            self.cpu_quota[container_name] = tmp/100000.0\n                quota = {'cpu':self.cpu_quota[container_name],'memory':self.mem_quota[container_name]}\n                #logger.info(quota)\n                workercinfo[container_name]['quota'] = quota\n            else:\n                logger.error(\"Cant't find config file %s\"%(confpath))\n                return False\n            self.cpu_last[container_name] = 0\n        # compute cpu used percent\n        cpu_use = {}\n        lastval = 0\n        try:\n            if not is_batch:\n                lastval = laststopcpuval[container_name]\n        except:\n            logger.warning(traceback.format_exc())\n        cpu_val += lastval\n        cpu_use['val'] = cpu_val\n        cpu_use['unit'] = cpu_unit\n        cpu_usedp = (float(cpu_val)-float(self.cpu_last[container_name]))/(self.cpu_quota[container_name]*self.interval*1.05)\n        cpu_use['hostpercent'] = (float(cpu_val)-float(self.cpu_last[container_name]))/(self.cores_num*self.interval*1.05)\n        if(cpu_usedp > 1 or cpu_usedp < 0):\n            cpu_usedp = 1\n        cpu_use['usedp'] = cpu_usedp\n        self.cpu_last[container_name] = cpu_val;\n        workercinfo[container_name]['cpu_use'] = cpu_use\n\n        if container_name not in increment.keys():\n            # initialize increment\n            increment[container_name] = {}\n            increment[container_name]['lastcputime'] = cpu_val\n            increment[container_name]['memincrement'] = 0\n\n        # deal with memory used data\n        memory = float(container.get_cgroup_item(\"memory.usage_in_bytes\"))\n        increment[container_name]['memincrement'] += memory / 1024 / 1024\n\n        mem_val = memory / 1024\n        mem_unit = 'KiB'\n        if mem_val > 1024:\n            mem_val /= 1024\n            mem_unit = 'MiB'\n        if mem_val > 1024:\n            mem_val /= 1024\n            mem_unit = 'GiB'\n\n        mem_use = {}\n        mem_use['val'] = float(\"%.2f\" % mem_val)\n        mem_use['unit'] = mem_unit\n        mem_use['usedp'] = memory / 1024 / self.mem_quota[container_name]\n        workercinfo[container_name]['mem_use'] = mem_use\n        # compute billing value during this running hour up to now\n        workercinfo[container_name]['basic_info']['billing_this_hour'] = self.billing_increment(container_name,False)\n\n        # deal with network used data\n        containerids = re.split(\"-\",container_name)\n        if not is_batch and len(containerids) >= 3 and (containerids[1] + \"-\" + containerids[2]) in self.net_stats.keys():\n            workercinfo[container_name]['net_stats'] = self.net_stats[containerids[1] + '-' + containerids[2]]\n            #logger.info(workercinfo[container_name]['net_stats'])\n\n        if not container_name in lastbillingtime.keys():\n            lastbillingtime[container_name] = int(running_time/self.billingtime)\n        lasttime = lastbillingtime[container_name]\n        #logger.info(lasttime)\n        # process real billing if running time reach an hour\n        if not is_batch and not int(running_time/self.billingtime) == lasttime:\n            #logger.info(\"billing:\"+str(float(cpu_val)))\n            lastbillingtime[container_name] = int(running_time/self.billingtime)\n            self.billing_increment(container_name)\n        #print(output)\n        #print(parts)\n        return True\n\n    # run function in the thread\n    def run(self):\n        global workercinfo\n        global workerinfo\n        cnt = 0\n        while not self.thread_stop:\n            self.collect_net_stats()\n            containers = self.list_container()\n            countR = 0\n            conlist = []\n            for container in containers:\n                # collect data of each container\n                if not container == '':\n                    conlist.append(container)\n                    if not container in workercinfo.keys():\n                        workercinfo[container] = {}\n                    try:\n                        success= self.collect_containerinfo(container)\n                        if(success):\n                            countR += 1\n                    except Exception as err:\n                        logger.warning(traceback.format_exc())\n                        logger.warning(err)\n            containers_num = len(containers)-1\n            concnt = {}\n            concnt['total'] = containers_num\n            concnt['running'] = countR\n            workerinfo['containers'] = concnt\n            time.sleep(self.interval)\n            if cnt == 0:\n                # update containers list on the worker each 5 times\n                workerinfo['containerslist'] = conlist\n            cnt = (cnt+1)%5\n            if self.test:\n                break\n        return\n\n    def stop(self):\n        self.thread_stop = True\n\n# the class is to colect monitoring data of the worker\nclass Collector(threading.Thread):\n\n    def __init__(self,test=False):\n        global workerinfo\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.interval = 1\n        self.test=test\n        self.gpu_info_count = 0\n        self.gpu_info_cache = None\n        workerinfo['concpupercent'] = {}\n        return\n\n    # collect memory used information\n    def collect_meminfo(self):\n        meminfo = psutil.virtual_memory()\n        memdict = {}\n        memdict['total'] = meminfo.total/1024\n        memdict['used'] = meminfo.used/1024\n        memdict['free'] = meminfo.free/1024\n        memdict['buffers'] = meminfo.buffers/1024\n        memdict['cached'] = meminfo.cached/1024\n        memdict['percent'] = meminfo.percent\n        #print(output)\n        #print(memparts)\n        return memdict\n\n    # collect cpu used information and processors information\n    def collect_cpuinfo(self):\n        cpuinfo = psutil.cpu_times_percent(interval=1,percpu=False)\n        cpuset = {}\n        cpuset['user'] = cpuinfo.user\n        cpuset['system'] = cpuinfo.system\n        cpuset['idle'] = cpuinfo.idle\n        cpuset['iowait'] = cpuinfo.iowait\n        # get processors information from /proc/cpuinfo\n        output = subprocess.check_output([\"cat /proc/cpuinfo\"],shell=True)\n        output = output.decode('utf-8')\n        parts = output.split('\\n')\n        info = []\n        idx = -1\n        for part in parts:\n            if not part == '':\n                key_val = re.split(':',part)\n                key = key_val[0].rstrip()\n                if key == 'processor':\n                    info.append({})\n                    idx += 1\n                val = key_val[1].lstrip()\n                if key=='processor' or key=='model name' or key=='core id' or key=='cpu MHz' or key=='cache size' or key=='physical id':\n                    info[idx][key] = val\n        return [cpuset, info]\n\n    # collect gpu used information\n    def collect_gpuinfo(self):\n        # load gpu price\n        batch_gpu_billing = env.getenv(\"BATCH_GPU_BILLING\")\n        gpu_price = {}\n        default_gpu_price = 100 # /cores*h\n        if batch_gpu_billing:\n            # examples: default:100,GeForce-GTX-1080-Ti:100,GeForce-GTX-2080-Ti:150,Tesla-V100-PCIE-16GB:200\n            billing_configs = batch_gpu_billing.split(',')\n            for config in billing_configs:\n                config_sp = config.split(':')\n                if config_sp[0] == 'default':\n                    default_gpu_price = int(config_sp[1])\n                else:\n                    gpu_price[config_sp[0]] = int(config_sp[1])\n        # reload gpu info\n        if self.gpu_info_count == 0 or self.gpu_info_cache is None:\n            self.gpu_info_cache = gputools.get_gpu_status()\n            gpu_names = gputools.get_gpu_names()\n            for index in range(len(self.gpu_info_cache)):\n                if index < len(gpu_names):\n                    self.gpu_info_cache[index]['name'] = gpu_names[index]\n                    self.gpu_info_cache[index]['price'] = gpu_price.get(gpu_names[index], default_gpu_price)\n        self.gpu_info_count = (self.gpu_info_count + 1) % 5\n        return self.gpu_info_cache\n\n    # collect disk used information\n    def collect_diskinfo(self):\n        global workercinfo\n        parts = psutil.disk_partitions()\n        setval = []\n        devices = {}\n        for part in parts:\n            # deal with each partition\n            if not part.device in devices:\n                devices[part.device] = 1\n                diskval = {}\n                diskval['device'] = part.device\n                diskval['mountpoint'] = part.mountpoint\n                try:\n                    usage = psutil.disk_usage(part.mountpoint)\n                    diskval['total'] = usage.total\n                    diskval['used'] = usage.used\n                    diskval['free'] = usage.free\n                    diskval['percent'] = usage.percent\n                    if(part.mountpoint.startswith('/opt/docklet/local/volume')):\n                        # the mountpoint indicate that the data is the disk used information of a container\n                        names = re.split('/',part.mountpoint)\n                        container = names[len(names)-1]\n                        if not container in workercinfo.keys():\n                            workercinfo[container] = {}\n                        workercinfo[container]['disk_use'] = diskval\n                    setval.append(diskval)  # make a list\n                except Exception as err:\n                    logger.warning(traceback.format_exc())\n                    logger.warning(err)\n        #print(output)\n        #print(diskparts)\n        return setval\n\n    # collect operating system information\n    def collect_osinfo(self):\n        uname = platform.uname()\n        osinfo = {}\n        osinfo['platform'] = platform.platform()\n        osinfo['system'] = uname.system\n        osinfo['node'] = uname.node\n        osinfo['release'] = uname.release\n        osinfo['version'] = uname.version\n        osinfo['machine'] = uname.machine\n        osinfo['processor'] = uname.processor\n        return osinfo\n\n    # run function in the thread\n    def run(self):\n        global workerinfo\n        workerinfo['osinfo'] = self.collect_osinfo()\n        while not self.thread_stop:\n            workerinfo['meminfo'] = self.collect_meminfo()\n            [cpuinfo,cpuconfig] = self.collect_cpuinfo()\n            workerinfo['cpuinfo'] = cpuinfo\n            workerinfo['cpuconfig'] = cpuconfig\n            workerinfo['gpuinfo'] = self.collect_gpuinfo()\n            workerinfo['diskinfo'] = self.collect_diskinfo()\n            workerinfo['running'] = True\n            time.sleep(self.interval)\n            if self.test:\n                break\n            #   print(self.etcdser.getkey('/meminfo/total'))\n        return\n\n    def stop(self):\n        self.thread_stop = True\n\n# the function used by rpc to fetch data from worker\ndef workerFetchInfo(master_ip):\n    global workerinfo\n    global workercinfo\n    global gateways_stats\n    global G_masterip\n    # tell the worker the ip address of the master\n    G_masterip = master_ip\n    return str([workerinfo, workercinfo, gateways_stats])\n\n# get owner name of a container\ndef get_owner(container_name):\n    names = container_name.split('-')\n    return names[0]\n\n# get cluster id of a container\ndef get_cluster(container_name):\n    names = container_name.split('-')\n    return names[1]\n\ndef count_port_mapping(vnode_name):\n    pms = PortMapping.query.filter_by(node_name=vnode_name).all()\n    return len(pms)\n\ndef save_billing_history(vnode_name, billing_history):\n    vnode_cluster_id = get_cluster(vnode_name)\n    try:\n        vcluster = VCluster.query.get(int(vnode_cluster_id))\n        billinghistory = BillingHistory.query.get(vnode_name)\n        if billinghistory is not None:\n            billinghistory.cpu = billing_history[\"cpu\"]\n            billinghistory.mem = billing_history[\"mem\"]\n            billinghistory.disk = billing_history[\"disk\"]\n            billinghistory.port = billing_history[\"port\"]\n        else:\n            billinghistory = BillingHistory(vnode_name,billing_history[\"cpu\"],billing_history[\"mem\"],billing_history[\"disk\"],billing_history[\"port\"])\n            vcluster.billing_history.append(billinghistory)\n        db.session.add(vcluster)\n        db.session.commit()\n    except Exception as err:\n        db.session.rollback()\n        logger.error(traceback.format_exc())\n    return\n\ndef get_billing_history(vnode_name):\n    billinghistory = BillingHistory.query.get(vnode_name)\n    if billinghistory is not None:\n        return dict(eval(str(billinghistory)))\n    else:\n        default = {}\n        default['cpu'] = 0\n        default['mem'] = 0\n        default['disk'] = 0\n        default['port'] = 0\n        return default\n\n# To record data when the status of containers change\nclass History_Manager:\n\n    def __init__(self):\n        try:\n            VNode.query.all()\n            History.query.all()\n        except:\n            db.create_all(bind='__all__')\n\n    def getAll(self):\n        return History.query.all()\n\n    # log to the database, it will record runnint time, cpu time, billing val and action\n    # action may be 'Create', 'Stop', 'Start', 'Recover', 'Delete'\n    def log(self,vnode_name,action):\n        global workercinfo\n        global laststopcpuval\n        res = VNode.query.filter_by(name=vnode_name).first()\n        if res is None:\n            vnode = VNode(vnode_name)\n            vnode.histories = []\n            db.session.add(vnode)\n            try:\n                db.session.commit()\n            except Exception as err:\n                db.session.rollback()\n                logger.error(traceback.format_exc())\n        vnode = VNode.query.get(vnode_name)\n        billing = 0\n        cputime = 0\n        runtime = 0\n        owner = get_owner(vnode_name)\n        try:\n            billing = int(workercinfo[vnode_name]['basic_info']['billing'])\n        except:\n            billing = 0\n        try:\n            cputime = float(workercinfo[vnode_name]['cpu_use']['val'])\n        except:\n            cputime = 0.0\n        try:\n            runtime = float(workercinfo[vnode_name]['basic_info']['RunningTime'])\n        except Exception as err:\n            #print(traceback.format_exc())\n            runtime = 0\n        history = History(action,runtime,cputime,billing)\n        vnode.histories.append(history)\n        if action == 'Stop' or action == 'Create':\n            laststopcpuval[vnode_name] = cputime\n            vnode.laststopcpuval = cputime\n            laststopruntime[vnode_name] = runtime\n            vnode.laststopruntime = runtime\n        db.session.add(history)\n        try:\n            db.session.commit()\n        except Exception as err:\n            db.session.rollback()\n            logger.error(traceback.format_exc())\n\n    def getHistory(self,vnode_name):\n        vnode = VNode.query.filter_by(name=vnode_name).first()\n        if vnode is None:\n            return []\n        else:\n            res = History.query.filter_by(vnode=vnode_name).all()\n            return list(eval(str(res)))\n\n    # get all created containers(including those have been deleted) of a owner\n    def getCreatedVNodes(self,owner):\n        vnodes = VNode.query.filter(VNode.name.startswith(owner)).all()\n        res = []\n        for vnode in vnodes:\n            tmp = {\"name\":vnode.name,\"billing\":vnode.billing}\n            res.append(tmp)\n        return res\n"
  },
  {
    "path": "src/worker/ossmounter.py",
    "content": "import abc\nimport subprocess, os\nfrom utils.log import logger\n\nclass OssMounter(object):\n    __metaclass__ = abc.ABCMeta\n\n    @staticmethod\n    def execute_cmd(cmd):\n        ret = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            msg = ret.stdout.decode(encoding=\"utf-8\")\n            logger.error(msg)\n            return [False,msg]\n        else:\n            return [True,\"\"]\n\n    @staticmethod\n    @abc.abstractmethod\n    def mount_oss(datapath, mount_info):\n        # mount oss\n        pass\n\n    @staticmethod\n    @abc.abstractmethod\n    def umount_oss(datapath, mount_info):\n        # umount oss\n        pass\n\nclass AliyunOssMounter(OssMounter):\n\n    @staticmethod\n    def mount_oss(datapath, mount_info):\n        # mount oss\n        try:\n            pwdfile = open(\"/etc/passwd-ossfs\",\"w\")\n            pwdfile.write(mount_info.remotePath+\":\"+mount_info.accessKey+\":\"+mount_info.secretKey+\"\\n\")\n            pwdfile.close()\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            return [False,msg]\n\n        cmd = \"chmod 640 /etc/passwd-ossfs\"\n        [success1, msg] = OssMounter.execute_cmd(cmd)\n        if not success1:\n            logger.error(\"Aliyun OSS mount chmod err:%s\" % msg)\n            return [False, msg]\n        mountpath = datapath+\"/Aliyun/\"+mount_info.remotePath\n        logger.info(\"Mount oss %s %s\" % (mount_info.remotePath, mountpath))\n        if not os.path.isdir(mountpath):\n            os.makedirs(mountpath)\n        cmd = \"ossfs %s %s -ourl=%s\" % (mount_info.remotePath, mountpath, mount_info.other)\n        [success, msg] = OssMounter.execute_cmd(cmd)\n        if not success:\n            logger.error(\"Aliyun OSS mount err:%s\" % msg)\n            return [False, msg]\n        return [True,\"\"]\n\n    @staticmethod\n    def umount_oss(datapath, mount_info):\n        mountpath = datapath+\"/Aliyun/\"+mount_info.remotePath\n        logger.info(\"UMount oss %s %s\" % (mount_info.remotePath, mountpath))\n        cmd = \"fusermount -u %s\" % (mountpath)\n        [success, msg] = OssMounter.execute_cmd(cmd)\n        if not success:\n            logger.error(\"Aliyun OSS umount err:%s\"%msg)\n            return [False,msg]\n        [success, msg] = OssMounter.execute_cmd(\"rm -rf %s\" % mountpath)\n        if not success:\n            logger.error(\"Aliyun OSS umount err:%s\"%msg)\n            return [False,msg]\n        return [True,\"\"]\n"
  },
  {
    "path": "src/worker/taskcontroller.py",
    "content": "#!/usr/bin/python3\nimport sys\nif sys.path[0].endswith(\"worker\"):\n    sys.path[0] = sys.path[0][:-6]\nfrom utils import env, tools\nconfig = env.getenv(\"CONFIG\")\n#config = \"/opt/docklet/local/docklet-running.conf\"\ntools.loadenv(config)\nfrom utils.log import initlogging\ninitlogging(\"docklet-taskcontroller\")\nfrom utils.log import logger\n\nfrom concurrent import futures\nimport grpc\n#from utils.log import logger\n#from utils import env\nimport json,lxc,subprocess,threading,os,time,traceback\nfrom utils import imagemgr,etcdlib,gputools\nfrom utils.lvmtool import sys_run\nfrom worker import ossmounter\nfrom protos import rpc_pb2, rpc_pb2_grpc\n\n_ONE_DAY_IN_SECONDS = 60 * 60 * 24\nMAX_RUNNING_TIME = _ONE_DAY_IN_SECONDS\n\ndef ip_to_int(addr):\n    [a, b, c, d] = addr.split('.')\n    return (int(a)<<24) + (int(b)<<16) + (int(c)<<8) + int(d)\n\ndef int_to_ip(num):\n    return str((num>>24)&255)+\".\"+str((num>>16)&255)+\".\"+str((num>>8)&255)+\".\"+str(num&255)\n\nclass TaskController(rpc_pb2_grpc.WorkerServicer):\n\n    def __init__(self):\n        rpc_pb2_grpc.WorkerServicer.__init__(self)\n        etcdaddr = env.getenv(\"ETCD\")\n        logger.info (\"using ETCD %s\" % etcdaddr )\n\n        clustername = env.getenv(\"CLUSTER_NAME\")\n        logger.info (\"using CLUSTER_NAME %s\" % clustername )\n\n        # init etcdlib client\n        try:\n            self.etcdclient = etcdlib.Client(etcdaddr, prefix = clustername)\n        except Exception:\n            logger.error (\"connect etcd failed, maybe etcd address not correct...\")\n            sys.exit(1)\n        else:\n            logger.info(\"etcd connected\")\n\n        # get master ip and report port\n        [success,masterip] = self.etcdclient.getkey(\"service/master\")\n        if not success:\n            logger.error(\"Fail to get master ip address.\")\n            sys.exit(1)\n        else:\n            self.master_ip = masterip\n            logger.info(\"Get master ip address: %s\" % (self.master_ip))\n        self.master_port = env.getenv('BATCH_MASTER_PORT')\n\n        self.imgmgr = imagemgr.ImageMgr()\n        self.fspath = env.getenv('FS_PREFIX')\n        self.confpath = env.getenv('DOCKLET_CONF')\n\n        self.taskmsgs = []\n        self.msgslock = threading.Lock()\n        self.report_interval = 2\n\n        self.lock = threading.Lock()\n        self.mount_lock = threading.Lock()\n        self.cons_gateway = env.getenv('BATCH_GATEWAY')\n        self.cons_ips = env.getenv('BATCH_NET')\n        logger.info(\"Batch gateway ip address %s\" % self.cons_gateway)\n        logger.info(\"Batch ip pools %s\" % self.cons_ips)\n\n        self.cidr = 32 - int(self.cons_ips.split('/')[1])\n        self.ipbase = ip_to_int(self.cons_ips.split('/')[0])\n        self.free_ips = []\n        for i in range(2, (1 << self.cidr) - 1):\n            self.free_ips.append(i)\n        logger.info(\"Free ip addresses pool %s\" % str(self.free_ips))\n\n        self.gpu_lock = threading.Lock()\n        self.gpu_status = {}\n        gpus = gputools.get_gpu_status()\n        for gpu in gpus:\n            self.gpu_status[gpu['id']] = \"\"\n\n        self.start_report()\n        logger.info('TaskController init success')\n\n    # Need Locks\n    def acquire_ip(self):\n        self.lock.acquire()\n        if len(self.free_ips) == 0:\n            return [False, \"No free ips\"]\n        ip = int_to_ip(self.ipbase + self.free_ips[0])\n        self.free_ips.remove(self.free_ips[0])\n        logger.info(str(self.free_ips))\n        self.lock.release()\n        return [True, ip + \"/\" + str(32 - self.cidr)]\n\n    # Need Locks\n    def release_ip(self,ipstr):\n        self.lock.acquire()\n        ipnum = ip_to_int(ipstr.split('/')[0]) - self.ipbase\n        self.free_ips.append(ipnum)\n        logger.info(str(self.free_ips))\n        self.lock.release()\n\n    def add_gpu_device(self, lxcname, gpu_need):\n        if gpu_need < 1:\n            return [True, \"\"]\n        self.gpu_lock.acquire()\n        use_gpus = []\n        for gpuid in self.gpu_status.keys():\n            if self.gpu_status[gpuid] == \"\" and gpu_need > 0:\n                use_gpus.append(gpuid)\n                gpu_need -= 1\n        if gpu_need > 0:\n            self.gpu_lock.release()\n            return [False, \"No free GPUs\"]\n        for gpuid in use_gpus:\n            self.gpu_status[gpuid] = lxcname\n        try:\n            gputools.add_device(lxcname, \"/dev/nvidiactl\")\n            gputools.add_device(lxcname, \"/dev/nvidia-uvm\")\n            for gpuid in use_gpus:\n                gputools.add_device(lxcname,\"/dev/nvidia\"+str(gpuid))\n                logger.info(\"Add gpu:\"+str(gpuid) +\" to lxc:\"+str(lxcname))\n        except Exception as e:\n            logger.error(traceback.format_exc())\n            for gpuid in use_gpus:\n                self.gpu_status[gpuid] = \"\"\n            self.gpu_lock.release()\n            return [False, \"Error occurs when adding gpu device.\"]\n\n        self.gpu_lock.release()\n        return [True, \"\"]\n\n    def release_gpu_device(self, lxcname):\n        self.gpu_lock.acquire()\n        for gpuid in self.gpu_status.keys():\n            if self.gpu_status[gpuid] == lxcname:\n                self.gpu_status[gpuid] = \"\"\n        self.gpu_lock.release()\n\n    #mount_oss\n    def mount_oss(self, datapath, mount_info):\n        self.mount_lock.acquire()\n        try:\n            for mount in mount_info:\n                provider = mount.provider\n                mounter = getattr(ossmounter,provider+\"OssMounter\",None)\n                if mounter is None:\n                    self.mount_lock.release()\n                    return [False, provider + \" doesn't exist!\"]\n                [success, msg] = mounter.mount_oss(datapath,mount)\n                if not success:\n                    self.mount_lock.release()\n                    return [False, msg]\n        except Exception as err:\n            self.mount_lock.release()\n            logger.error(traceback.format_exc())\n            return [False,\"\"]\n\n        self.mount_lock.release()\n        return [True,\"\"]\n\n    #umount oss\n    def umount_oss(self, datapath, mount_info):\n        try:\n            for mount in mount_info:\n                provider = mount.provider\n                mounter = getattr(ossmounter,provider+\"OssMounter\",None)\n                if mounter is None:\n                    return [False, provider + \" doesn't exist!\"]\n                [success, msg] = mounter.umount_oss(datapath,mount)\n                if not success:\n                    return [False, msg]\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            return [False,\"\"]\n    #accquire ip and create a container\n    def create_container(self,instanceid,username,image,lxcname,quota):\n        # acquire ip\n        [status, ip] = self.acquire_ip()\n        if not status:\n            return [False, ip]\n\n        # prepare image and filesystem\n        status = self.imgmgr.prepareFS(username,image,lxcname,str(quota.disk))\n        if not status:\n            self.release_ip(ip)\n            return [False, \"Create container for batch failed when preparing filesystem\"]\n\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxcname\n\n        if not os.path.isdir(\"%s/global/users/%s\" % (self.fspath,username)):\n            path = env.getenv('DOCKLET_LIB')\n            subprocess.call([path+\"/master/userinit.sh\", username])\n            logger.info(\"user %s directory not found, create it\" % username)\n        sys_run(\"mkdir -p /var/lib/lxc/%s\" % lxcname)\n        logger.info(\"generate config file for %s\" % lxcname)\n\n        def config_prepare(content):\n            content = content.replace(\"%ROOTFS%\",rootfs)\n            content = content.replace(\"%HOSTNAME%\",\"batch-%s\" % str(instanceid))\n            content = content.replace(\"%CONTAINER_MEMORY%\",str(quota.memory))\n            content = content.replace(\"%CONTAINER_CPU%\",str(quota.cpu*100000))\n            content = content.replace(\"%FS_PREFIX%\",self.fspath)\n            content = content.replace(\"%LXCSCRIPT%\",env.getenv(\"LXC_SCRIPT\"))\n            content = content.replace(\"%USERNAME%\",username)\n            content = content.replace(\"%LXCNAME%\",lxcname)\n            content = content.replace(\"%IP%\",ip)\n            content = content.replace(\"%GATEWAY%\",self.cons_gateway)\n            return content\n\n        logger.info(self.confpath)\n        conffile = open(self.confpath+\"/container.batch.conf\", 'r')\n        conftext = conffile.read()\n        conffile.close()\n\n        conftext = config_prepare(conftext)\n\n        conffile = open(\"/var/lib/lxc/%s/config\" % lxcname, 'w')\n        conffile.write(conftext)\n        conffile.close()\n        return [True, ip]\n\n    def process_task(self, request, context):\n        logger.info('excute task with parameter: ' + str(request))\n        taskid = request.id\n        instanceid = request.instanceid\n\n        # get config from request\n        command = request.parameters.command.commandLine #'/root/getenv.sh'  #parameter['Parameters']['Command']['CommandLine']\n        #envs = {'MYENV1':'MYVAL1', 'MYENV2':'MYVAL2'} #parameters['Parameters']['Command']['EnvVars']\n        pkgpath = request.parameters.command.packagePath\n        envs = request.parameters.command.envVars\n        envs['taskid'] = str(taskid)\n        envs['instanceid'] = str(instanceid)\n        image = {}\n        image['name'] = request.cluster.image.name\n        if request.cluster.image.type == rpc_pb2.Image.PRIVATE:\n            image['type'] = 'private'\n        elif request.cluster.image.type == rpc_pb2.Image.PUBLIC:\n            image['type'] = 'public'\n        else:\n            image['type'] = 'base'\n        image['owner'] = request.cluster.image.owner\n        username = request.username\n        token = request.token\n        lxcname = '%s-batch-%s-%s-%s' % (username,taskid,str(instanceid),token)\n        instance_type =  request.cluster.instance\n        mount_list = request.cluster.mount\n        outpath = [request.parameters.stdoutRedirectPath,request.parameters.stderrRedirectPath]\n        timeout = request.timeout\n        gpu_need = int(request.cluster.instance.gpu)\n        reused = request.reused\n\n        #create container\n        [success, ip] = self.create_container(instanceid, username, image, lxcname, instance_type)\n        if not success:\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED, message=ip)\n\n        #mount oss\n        self.mount_oss(\"%s/global/users/%s/oss\" % (self.fspath,username), mount_list)\n        conffile = open(\"/var/lib/lxc/%s/config\" % lxcname, 'a+')\n        mount_str = \"lxc.mount.entry = %s/global/users/%s/oss/%s %s/root/oss/%s none bind,rw,create=dir 0 0\"\n        for mount in mount_list:\n            conffile.write(\"\\n\"+ mount_str % (self.fspath, username, mount.remotePath, rootfs, mount.remotePath))\n        conffile.close()\n\n\n        logger.info(\"Start container %s...\" % lxcname)\n        #container = lxc.Container(lxcname)\n        ret = subprocess.run('lxc-start -n %s'%lxcname,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            logger.error('start container %s failed' % lxcname)\n            self.release_ip(ip)\n            self.imgmgr.deleteFS(lxcname)\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED,message=\"Can't start the container\")\n\n        logger.info('start container %s success' % lxcname)\n\n        #add GPU\n        [success, msg] = self.add_gpu_device(lxcname,gpu_need)\n        if not success:\n            logger.error(\"Fail to add gpu device. \" + msg)\n            container.stop()\n            self.release_ip(ip)\n            self.imgmgr.deleteFS(lxcname)\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED,message=\"Fail to add gpu device. \" + msg)\n\n        thread = threading.Thread(target = self.execute_task, args=(username,taskid,instanceid,envs,lxcname,pkgpath,command,timeout,outpath,ip,token,mount_list))\n        thread.setDaemon(True)\n        thread.start()\n\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    def write_output(self,lxcname,tmplogpath,filepath):\n        cmd = \"lxc-attach -n \" + lxcname + \" -- mv %s %s\"\n        if filepath == \"\" or filepath == \"/root/nfs/batch_{jobid}/\" or os.path.abspath(\"/root/nfs/\"+tmplogpath) == os.path.abspath(filepath):\n            return [True,\"\"]\n        ret = subprocess.run(cmd % (\"/root/nfs/\"+tmplogpath,filepath),stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            msg = ret.stdout.decode(encoding=\"utf-8\")\n            logger.error(msg)\n            return [False,msg]\n        logger.info(\"Succeed to moving nfs/%s to %s\" % (tmplogpath,filepath))\n        return [True,\"\"]\n\n    def execute_task(self,username,taskid,instanceid,envs,lxcname,pkgpath,command,timeout,outpath,ip,token,mount_info):\n        lxcfspath = \"/var/lib/lxc/\"+lxcname+\"/rootfs/\"\n        scriptname = \"batch_job.sh\"\n        try:\n            scriptfile = open(lxcfspath+\"root/\"+scriptname,\"w\")\n            scriptfile.write(\"#!/bin/bash\\n\")\n            scriptfile.write(\"cd \"+str(pkgpath)+\"\\n\")\n            scriptfile.write(command)\n            scriptfile.close()\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            logger.error(\"Fail to write script file with taskid(%s) instanceid(%s)\" % (str(taskid),str(instanceid)))\n        else:\n            try:\n                job_id = taskid.split('_')[1]\n            except Exception as e:\n                logger.error(traceback.format_exc())\n                job_id = \"_none\"\n            jobdir = \"batch_\" + job_id\n            logdir = \"%s/global/users/%s/data/\" % (self.fspath,username) + jobdir\n            if not os.path.exists(logdir):\n                logger.info(\"Directory:%s not exists, create it.\" % logdir)\n                os.mkdir(logdir)\n            stdoutname = str(taskid)+\"_\"+str(instanceid)+\"_stdout.txt\"\n            stderrname = str(taskid)+\"_\"+str(instanceid)+\"_stderr.txt\"\n            try:\n                stdoutfile = open(logdir+\"/\"+stdoutname,\"w\")\n                stderrfile = open(logdir+\"/\"+stderrname,\"w\")\n                logger.info(\"Create stdout(%s) and stderr(%s) file to log\" % (stdoutname, stderrname))\n            except Exception as e:\n                logger.error(traceback.format_exc())\n                stdoutfile = None\n                stderrfile = None\n\n            cmd = \"lxc-attach -n \" + lxcname\n            for envkey,envval in envs.items():\n                cmd = cmd + \" -v %s=%s\" % (envkey,envval)\n            cmd = cmd + \" -- /bin/bash \\\"\" + \"/root/\" + scriptname + \"\\\"\"\n            logger.info('run task with command - %s' % cmd)\n            p = subprocess.Popen(cmd,stdout=stdoutfile,stderr=stderrfile, shell=True)\n            #logger.info(p)\n            if timeout == 0:\n                to = MAX_RUNNING_TIME\n            else:\n                to = timeout\n            while p.poll() is None and to > 0:\n                time.sleep(min(2,to))\n                to -= 2\n            if p.poll() is None:\n                p.kill()\n                logger.info(\"Running time(%d) is out. Task(%s-%s-%s) will be killed.\" % (timeout,str(taskid),str(instanceid),token))\n                self.add_msg(taskid,username,instanceid,rpc_pb2.TIMEOUT,token,\"Running time is out.\")\n            else:\n                [success1,msg1] = self.write_output(lxcname,jobdir+\"/\"+stdoutname,outpath[0])\n                [success2,msg2] = self.write_output(lxcname,jobdir+\"/\"+stderrname,outpath[1])\n                if not success1 or not success2:\n                    if not success1:\n                        msg = msg1\n                    else:\n                        msg = msg2\n                    logger.info(\"Output error on Task(%s-%s-%s).\" % (str(taskid),str(instanceid),token))\n                    self.add_msg(taskid,username,instanceid,rpc_pb2.OUTPUTERROR,token,msg)\n                else:\n                    if p.poll() == 0:\n                        logger.info(\"Task(%s-%s-%s) completed.\" % (str(taskid),str(instanceid),token))\n                        self.add_msg(taskid,username,instanceid,rpc_pb2.COMPLETED,token,\"\")\n                    else:\n                        logger.info(\"Task(%s-%s-%s) failed.\" % (str(taskid),str(instanceid),token))\n                        self.add_msg(taskid,username,instanceid,rpc_pb2.FAILED,token,\"\")\n\n        container = lxc.Container(lxcname)\n        if container.stop():\n            logger.info(\"stop container %s success\" % lxcname)\n        else:\n            logger.error(\"stop container %s failed\" % lxcname)\n\n        logger.info(\"deleting container:%s\" % lxcname)\n        if self.imgmgr.deleteFS(lxcname):\n            logger.info(\"delete container %s success\" % lxcname)\n        else:\n            logger.error(\"delete container %s failed\" % lxcname)\n\n        logger.info(\"release ip address %s\" % ip)\n        self.release_ip(ip)\n        self.release_gpu_device(lxcname)\n\n        #umount oss\n        self.umount_oss(\"%s/global/users/%s/oss\" % (self.fspath,username), mount_info)\n\n    def stop_tasks(self, request, context):\n        for msg in request.taskmsgs:\n            lxcname = '%s-batch-%s-%s-%s' % (msg.username,msg.taskid,str(msg.instanceid),msg.token)\n            logger.info(\"Stop the task with lxc:\"+lxcname)\n            subprocess.run(\"lxc-stop -k -n %s\" % lxcname, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    def add_msg(self,taskid,username,instanceid,status,token,errmsg):\n        self.msgslock.acquire()\n        try:\n            self.taskmsgs.append(rpc_pb2.TaskMsg(taskid=str(taskid),username=username,instanceid=int(instanceid),instanceStatus=status,token=token,errmsg=errmsg))\n        except Exception as err:\n            logger.error(traceback.format_exc())\n        self.msgslock.release()\n        #logger.info(str(self.taskmsgs))\n\n    def report_msg(self):\n        channel = grpc.insecure_channel(self.master_ip+\":\"+self.master_port)\n        stub = rpc_pb2_grpc.MasterStub(channel)\n        while True:\n            self.msgslock.acquire()\n            reportmsg = rpc_pb2.ReportMsg(taskmsgs = self.taskmsgs)\n            try:\n                response = stub.report(reportmsg)\n                logger.info(\"Response from master by reporting: \"+str(response.status)+\" \"+response.message)\n            except Exception as err:\n                logger.error(traceback.format_exc())\n            self.taskmsgs = []\n            self.msgslock.release()\n            time.sleep(self.report_interval)\n\n    def start_report(self):\n        thread = threading.Thread(target = self.report_msg, args=())\n        thread.setDaemon(True)\n        thread.start()\n        logger.info(\"Start to report task messages to master every %d seconds.\" % self.report_interval)\n\n\ndef TaskControllerServe():\n    max_threads = int(env.getenv('BATCH_MAX_THREAD_WORKER'))\n    worker_port = int(env.getenv('BATCH_WORKER_PORT'))\n    logger.info(\"Max Threads on a worker is %d\" % max_threads)\n    server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_threads))\n    rpc_pb2_grpc.add_WorkerServicer_to_server(TaskController(), server)\n    server.add_insecure_port('[::]:'+str(worker_port))\n    server.start()\n    logger.info(\"Start TaskController Servicer on port:%d\" % worker_port)\n    try:\n        while True:\n            time.sleep(_ONE_DAY_IN_SECONDS)\n    except KeyboardInterrupt:\n        server.stop(0)\n\nif __name__ == \"__main__\":\n    TaskControllerServe()\n"
  },
  {
    "path": "src/worker/taskworker.py",
    "content": "#!/usr/bin/python3\nimport sys\nif sys.path[0].endswith(\"worker\"):\n    sys.path[0] = sys.path[0][:-6]\nfrom utils import env, tools\nconfig = env.getenv(\"CONFIG\")\n#config = \"/opt/docklet/local/docklet-running.conf\"\ntools.loadenv(config)\nfrom utils.log import initlogging\ninitlogging(\"docklet-taskworker\")\nfrom utils.log import logger\n\nfrom concurrent import futures\nimport grpc\n#from utils.log import logger\n#from utils import env\nimport json,lxc,subprocess,threading,os,time,traceback\nfrom utils import imagemgr,etcdlib,gputools\nfrom utils.lvmtool import sys_run\nfrom worker import ossmounter\nfrom protos import rpc_pb2, rpc_pb2_grpc\nfrom utils.nettools import netcontrol\nfrom master.network import getip\n\n_ONE_DAY_IN_SECONDS = 60 * 60 * 24\nMAX_RUNNING_TIME = _ONE_DAY_IN_SECONDS\n\nclass TaskWorker(rpc_pb2_grpc.WorkerServicer):\n\n    def __init__(self):\n        rpc_pb2_grpc.WorkerServicer.__init__(self)\n        etcdaddr = env.getenv(\"ETCD\")\n        logger.info (\"using ETCD %s\" % etcdaddr )\n\n        clustername = env.getenv(\"CLUSTER_NAME\")\n        logger.info (\"using CLUSTER_NAME %s\" % clustername )\n\n        # init etcdlib client\n        try:\n            self.etcdclient = etcdlib.Client(etcdaddr, prefix = clustername)\n        except Exception:\n            logger.error (\"connect etcd failed, maybe etcd address not correct...\")\n            sys.exit(1)\n        else:\n            logger.info(\"etcd connected\")\n\n        # get master ip and report port\n        [success,masterip] = self.etcdclient.getkey(\"service/master\")\n        if not success:\n            logger.error(\"Fail to get master ip address.\")\n            sys.exit(1)\n        else:\n            self.master_ip = masterip\n            logger.info(\"Get master ip address: %s\" % (self.master_ip))\n        self.master_port = env.getenv('BATCH_MASTER_PORT')\n\n        # get worker ip\n        self.worker_ip = getip(env.getenv('NETWORK_DEVICE'))\n        logger.info(\"Worker ip is :%s\"%self.worker_ip)\n\n        self.imgmgr = imagemgr.ImageMgr()\n        self.fspath = env.getenv('FS_PREFIX')\n        self.confpath = env.getenv('DOCKLET_CONF')\n        self.rm_all_batch_containers()\n\n        self.taskmsgs = []\n        self.msgslock = threading.Lock()\n        self.report_interval = 2\n\n        self.lock = threading.Lock()\n        self.mount_lock = threading.Lock()\n\n        self.gpu_lock = threading.Lock()\n        self.gpu_status = {}\n        gpus = gputools.get_gpu_status()\n        for gpu in gpus:\n            self.gpu_status[gpu['id']] = \"\"\n\n        self.start_report()\n        logger.info('TaskWorker init success')\n\n    def stop_and_rm_containers(self,lxcname):\n        logger.info(\"Stop the container with name:\"+lxcname)\n        subprocess.run(\"lxc-stop -k -n %s\" % lxcname, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n        lxcpath = \"/var/lib/lxc/%s\" % lxcname\n        try:\n            mount_info = []\n            for provider in os.listdir(lxcpath+\"/oss\"):\n                for bkname in os.listdir(lxcpath+\"/oss/\"+provider):\n                    mount_info.append(rpc_pb2.Mount(provider=provider,remotePath=bkname))\n            self.umount_oss(lxcpath+\"/oss\", mount_info)\n        except Exception as err:\n            logger.info(err)\n            pass\n        return self.imgmgr.deleteFS(lxcname)\n\n    def rm_all_batch_containers(self):\n        for con in lxc.list_containers():\n            keys = con.split('-')\n            if len(keys) < 2 or keys[1] != 'batch':\n                continue\n            if self.stop_and_rm_containers(con):\n                logger.info(\"delete container %s success\" % con)\n            else:\n                logger.error(\"delete container %s failed\" % con)\n\n    def add_gpu_device(self, lxcname, gpu_need):\n        if gpu_need < 1:\n            return [True, \"\"]\n        self.gpu_lock.acquire()\n        use_gpus = []\n        for gpuid in self.gpu_status.keys():\n            if self.gpu_status[gpuid] == \"\" and gpu_need > 0:\n                use_gpus.append(gpuid)\n                gpu_need -= 1\n        if gpu_need > 0:\n            self.gpu_lock.release()\n            return [False, \"No free GPUs\"]\n        for gpuid in use_gpus:\n            self.gpu_status[gpuid] = lxcname\n        try:\n            gputools.add_device(lxcname, \"/dev/nvidiactl\")\n            gputools.add_device(lxcname, \"/dev/nvidia-uvm\")\n            for gpuid in use_gpus:\n                gputools.add_device(lxcname,\"/dev/nvidia\"+str(gpuid))\n                logger.info(\"Add gpu:\"+str(gpuid) +\" to lxc:\"+str(lxcname))\n        except Exception as e:\n            logger.error(traceback.format_exc())\n            for gpuid in use_gpus:\n                self.gpu_status[gpuid] = \"\"\n            self.gpu_lock.release()\n            return [False, \"Error occurs when adding gpu device.\"]\n\n        self.gpu_lock.release()\n        return [True, \"\"]\n\n    def release_gpu_device(self, lxcname):\n        self.gpu_lock.acquire()\n        for gpuid in self.gpu_status.keys():\n            if self.gpu_status[gpuid] == lxcname:\n                self.gpu_status[gpuid] = \"\"\n        self.gpu_lock.release()\n\n    #mount_oss\n    def mount_oss(self, datapath, mount_info):\n        self.mount_lock.acquire()\n        try:\n            for mount in mount_info:\n                provider = mount.provider\n                mounter = getattr(ossmounter,provider+\"OssMounter\",None)\n                if mounter is None:\n                    self.mount_lock.release()\n                    return [False, provider + \" doesn't exist!\"]\n                [success, msg] = mounter.mount_oss(datapath,mount)\n                if not success:\n                    self.mount_lock.release()\n                    return [False, msg]\n        except Exception as err:\n            self.mount_lock.release()\n            logger.error(traceback.format_exc())\n            return [False,\"\"]\n\n        self.mount_lock.release()\n        return [True,\"\"]\n\n    #umount oss\n    def umount_oss(self, datapath, mount_info):\n        try:\n            for mount in mount_info:\n                provider = mount.provider\n                mounter = getattr(ossmounter,provider+\"OssMounter\",None)\n                if mounter is None:\n                    return [False, provider + \" doesn't exist!\"]\n                [success, msg] = mounter.umount_oss(datapath,mount)\n                if not success:\n                    return [False, msg]\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            return [False,\"\"]\n\n    def start_vnode(self, request, context):\n        logger.info('start vnode with config: ' + str(request))\n        taskid = request.taskid\n        vnodeid = request.vnodeid\n\n        envs = {}\n        envs['taskid'] = str(taskid)\n        envs['vnodeid'] = str(vnodeid)\n        image = {}\n        image['name'] = request.vnode.image.name\n        if request.vnode.image.type == rpc_pb2.Image.PRIVATE:\n            image['type'] = 'private'\n        elif request.vnode.image.type == rpc_pb2.Image.PUBLIC:\n            image['type'] = 'public'\n        else:\n            image['type'] = 'base'\n        image['owner'] = request.vnode.image.owner\n        username = request.username\n        lxcname = '%s-batch-%s-%s' % (username,taskid,str(vnodeid))\n        instance_type =  request.vnode.instance\n        mount_list = request.vnode.mount\n        gpu_need = int(request.vnode.instance.gpu)\n        ipaddr = request.vnode.network.ipaddr\n        gateway = request.vnode.network.gateway\n        brname = request.vnode.network.brname\n        masterip = request.vnode.network.masterip\n        hostname = request.vnode.hostname\n\n        #create container\n        [success, msg] = self.create_container(taskid, vnodeid, username, image, lxcname, instance_type, ipaddr, gateway, brname, hostname)\n        if not success:\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED, message=msg)\n\n        #mount oss\n        lxcpath = \"/var/lib/lxc/%s\" % lxcname\n        rootfs = lxcpath + \"/rootfs\"\n        self.mount_oss(lxcpath + \"/oss\", mount_list)\n        conffile = open(lxcpath + \"/config\", 'a+')\n        mount_str = \"lxc.mount.entry = \"+ lxcpath +\"/oss/%s/%s %s/root/oss/%s none bind,rw,create=dir 0 0\"\n        for mount in mount_list:\n            conffile.write(\"\\n\"+ mount_str % (mount.provider, mount.remotePath, rootfs, mount.remotePath))\n        conffile.close()\n\n        logger.info(\"Start container %s...\" % lxcname)\n        container = lxc.Container(lxcname)\n        ret = subprocess.run('lxc-start -n %s'%lxcname,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            logger.error('start container %s failed' % lxcname)\n            self.umount_oss(\"/var/lib/lxc/%s/oss\" % (lxcname), mount_list)\n            self.imgmgr.deleteFS(lxcname)\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED,message=\"Can't start the container(%s)\"%lxcname)\n\n        logger.info('start container %s success' % lxcname)\n\n        if masterip != self.worker_ip:\n            netcontrol.setup_gre(brname, masterip)\n\n        #add GPU\n        [success, msg] = self.add_gpu_device(lxcname,gpu_need)\n        if not success:\n            logger.error(\"Fail to add gpu device. \" + msg)\n            container.stop()\n            self.umount_oss(\"/var/lib/lxc/%s/oss\" % (lxcname), mount_list)\n            self.imgmgr.deleteFS(lxcname)\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED,message=\"Fail to add gpu device. \" + msg)\n\n        #start ssh service\n        cmd = \"lxc-attach -n %s -- service ssh start\" % lxcname\n        ret = subprocess.run(cmd,stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            logger.error('Fail to start ssh service of container %s' % lxcname)\n            container.stop()\n            self.umount_oss(\"/var/lib/lxc/%s/oss\" % (lxcname), mount_list)\n            self.imgmgr.deleteFS(lxcname)\n            return rpc_pb2.Reply(status=rpc_pb2.Reply.REFUSED,message=\"Fail to start ssh service. lxc(%s)\"%lxcname)\n\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    def start_task(self, request, context):\n        logger.info('start task with config: ' + str(request))\n        taskid = request.taskid\n        username = request.username\n        vnodeid = request.vnodeid\n        # get config from request\n        command = request.parameters.command.commandLine #'/root/getenv.sh'  #parameter['Parameters']['Command']['CommandLine']\n        #envs = {'MYENV1':'MYVAL1', 'MYENV2':'MYVAL2'} #parameters['Parameters']['Command']['EnvVars']\n        pkgpath = request.parameters.command.packagePath\n        envs = request.parameters.command.envVars\n        envs['taskid'] = str(taskid)\n        envs['vnodeid'] = str(vnodeid)\n        timeout = request.timeout\n        token = request.token\n        outpath = [request.parameters.stdoutRedirectPath,request.parameters.stderrRedirectPath]\n        lxcname = '%s-batch-%s-%s' % (username,taskid,str(vnodeid))\n\n        thread = threading.Thread(target = self.execute_task, args=(username,taskid,vnodeid,envs,lxcname,pkgpath,command,timeout,outpath,token))\n        thread.setDaemon(True)\n        thread.start()\n\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    def stop_task(self, request, context):\n        logger.info('stop task with config: ' + str(request))\n        taskid = request.taskid\n        username = request.username\n        vnodeid = request.vnodeid\n        lxcname = '%s-batch-%s-%s' % (username,taskid,str(vnodeid))\n        logger.info(\"Stop the task with lxc:\"+lxcname)\n        subprocess.run(\"lxc-stop -k -n %s\" % lxcname, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    # stop and remove container\n    def stop_vnode(self, request, context):\n        logger.info('stop vnode with config: ' + str(request))\n        taskid = request.taskid\n        username = request.username\n        vnodeid = request.vnodeid\n        brname = request.vnode.network.brname\n        mount_list = request.vnode.mount\n        lxcname = '%s-batch-%s-%s' % (username,taskid,str(vnodeid))\n\n        logger.info(\"Stop the task with lxc:\"+lxcname)\n        container = lxc.Container(lxcname)\n        if container.stop():\n            logger.info(\"stop container %s success\" % lxcname)\n        else:\n            logger.error(\"stop container %s failed\" % lxcname)\n\n        #umount oss\n        self.umount_oss(\"/var/lib/lxc/%s/oss\" % (lxcname), mount_list)\n\n        logger.info(\"deleting container:%s\" % lxcname)\n        if self.imgmgr.deleteFS(lxcname):\n            logger.info(\"delete container %s success\" % lxcname)\n        else:\n            logger.error(\"delete container %s failed\" % lxcname)\n\n        #del ovs bridge\n        if brname is not None:\n            netcontrol.del_bridge(brname)\n\n        #release gpu\n        self.release_gpu_device(lxcname)\n\n        return rpc_pb2.Reply(status=rpc_pb2.Reply.ACCEPTED,message=\"\")\n\n    def prepare_hook_conf(self, conf_path, env_dict):\n        try:\n            confile = open(conf_path, \"w\")\n            for k,v in env_dict.items():\n                confile.write(\"%s=%s\\n\"%(k,v))\n            confile.close()\n        except Exception as e:\n            logger.error(traceback.format_exc())\n            return [False, e]\n        return [True, \"\"]\n\n    #accquire ip and create a container\n    def create_container(self,taskid,vnodeid,username,image,lxcname,quota,ipaddr,gateway,brname,hostname):\n        # prepare image and filesystem\n        status = self.imgmgr.prepareFS(username,image,lxcname,str(quota.disk))\n        if not status:\n            return [False, \"Create container for batch failed when preparing filesystem\"]\n\n        rootfs = \"/var/lib/lxc/%s/rootfs\" % lxcname\n\n        if not os.path.isdir(\"%s/global/users/%s\" % (self.fspath,username)):\n            path = env.getenv('DOCKLET_LIB')\n            subprocess.call([path+\"/master/userinit.sh\", username])\n            logger.info(\"user %s directory not found, create it\" % username)\n        sys_run(\"mkdir -p /var/lib/lxc/%s\" % lxcname)\n        logger.info(\"generate config file for %s\" % lxcname)\n\n        def config_prepare(content):\n            content = content.replace(\"%ROOTFS%\",rootfs)\n            content = content.replace(\"%HOSTNAME%\",hostname)\n            content = content.replace(\"%TASKID%\",taskid)\n            content = content.replace(\"%CONTAINER_MEMORY%\",str(quota.memory))\n            content = content.replace(\"%CONTAINER_CPU%\",str(quota.cpu*100000))\n            content = content.replace(\"%FS_PREFIX%\",self.fspath)\n            content = content.replace(\"%LXCSCRIPT%\",env.getenv(\"LXC_SCRIPT\"))\n            content = content.replace(\"%USERNAME%\",username)\n            content = content.replace(\"%LXCNAME%\",lxcname)\n            content = content.replace(\"%VETHPAIR%\",str(taskid)+\"-\"+str(vnodeid))\n            content = content.replace(\"%IP%\",ipaddr)\n            content = content.replace(\"%GATEWAY%\",gateway)\n            return content\n\n        logger.info(self.confpath)\n        conffile = open(self.confpath+\"/container.batch.conf\", 'r')\n        conftext = conffile.read()\n        conffile.close()\n\n        conftext = config_prepare(conftext)\n\n        conffile = open(\"/var/lib/lxc/%s/config\" % lxcname, 'w')\n        conffile.write(conftext)\n        conffile.close()\n\n        hook_env = {}\n        hook_env['Bridge'] = brname\n        hook_env['HNAME'] = hostname\n        return self.prepare_hook_conf(rootfs+\"/../env.conf\",hook_env)\n\n    def write_output(self,lxcname,tmplogpath,filepath):\n        cmd = \"lxc-attach -n \" + lxcname + \" -- mv %s %s\"\n        if filepath == \"\" or filepath == \"/root/nfs/batch_{jobid}/\" or os.path.abspath(\"/root/nfs/\"+tmplogpath) == os.path.abspath(filepath):\n            return [True,\"\"]\n        ret = subprocess.run(cmd % (\"/root/nfs/\"+tmplogpath,filepath),stdout=subprocess.PIPE,stderr=subprocess.STDOUT, shell=True)\n        if ret.returncode != 0:\n            msg = ret.stdout.decode(encoding=\"utf-8\")\n            logger.error(msg)\n            return [False,msg]\n        logger.info(\"Succeed to moving nfs/%s to %s\" % (tmplogpath,filepath))\n        return [True,\"\"]\n\n    def execute_task(self,username,taskid,vnodeid,envs,lxcname,pkgpath,command,timeout,outpath,token):\n        lxcfspath = \"/var/lib/lxc/\"+lxcname+\"/rootfs/\"\n        scriptname = \"batch_job.sh\"\n        try:\n            scriptfile = open(lxcfspath+\"root/\"+scriptname,\"w\")\n            scriptfile.write(\"#!/bin/bash\\n\")\n            scriptfile.write(\"cd \"+str(pkgpath)+\"\\n\")\n            scriptfile.write(command)\n            scriptfile.close()\n        except Exception as err:\n            logger.error(traceback.format_exc())\n            logger.error(\"Fail to write script file with taskid(%s) vnodeid(%s)\" % (str(taskid),str(vnodeid)))\n        else:\n            try:\n                job_id = taskid.split('_')[0]\n            except Exception as e:\n                logger.error(traceback.format_exc())\n                job_id = \"_none\"\n            jobdir = \"batch_\" + job_id\n            logdir = \"%s/global/users/%s/data/\" % (self.fspath,username) + jobdir\n            try:\n                os.mkdir(logdir)\n            except Exception as e:\n                logger.info(\"Error when creating logdir :%s \"+str(e))\n            stdoutname = str(taskid)+\"_\"+str(vnodeid)+\"_stdout.txt\"\n            stderrname = str(taskid)+\"_\"+str(vnodeid)+\"_stderr.txt\"\n            try:\n                stdoutfile = open(logdir+\"/\"+stdoutname,\"w\")\n                stderrfile = open(logdir+\"/\"+stderrname,\"w\")\n                logger.info(\"Create stdout(%s) and stderr(%s) file to log\" % (stdoutname, stderrname))\n            except Exception as e:\n                logger.error(traceback.format_exc())\n                stdoutfile = None\n                stderrfile = None\n\n            cmd = \"lxc-attach -n \" + lxcname\n            for envkey,envval in envs.items():\n                cmd = cmd + \" -v %s=%s\" % (envkey,envval)\n            cmd = cmd + \" -- /bin/bash \\\"\" + \"/root/\" + scriptname + \"\\\"\"\n            logger.info('run task with command - %s' % cmd)\n            #p = subprocess.Popen(cmd,stdout=stdoutfile,stderr=stderrfile, shell=True)\n            #logger.info(p)\n            if timeout == 0:\n                timeout = MAX_RUNNING_TIME\n            try:\n                ret = subprocess.run(cmd, stdout=stdoutfile, stderr=stderrfile, shell=True, timeout = timeout)\n            except subprocess.TimeoutExpired as e:\n                logger.info(\"Running time(%d) is out. Task(%s-%s-%s) will be killed.\" % (timeout,str(taskid),str(vnodeid),token))\n                self.add_msg(taskid,username,vnodeid,rpc_pb2.TIMEOUT,token,\"Running time(%ds) is out.\" % timeout)\n            except Exception as e:\n                logger.error(traceback.format_exc())\n                logger.info(\"Someting is wrong:%s. Task(%s-%s-%s) will be killed.\" % (str(e),str(taskid),str(vnodeid),token))\n                self.add_msg(taskid,username,vnodeid,rpc_pb2.FAILED,token,\"Runtime Error. More information in stderr log.\")\n            else:\n                [success1,msg1] = self.write_output(lxcname,jobdir+\"/\"+stdoutname,outpath[0])\n                [success2,msg2] = self.write_output(lxcname,jobdir+\"/\"+stderrname,outpath[1])\n                if not success1 or not success2:\n                    if not success1:\n                        msg = msg1\n                    else:\n                        msg = msg2\n                    logger.info(\"Output error on Task(%s-%s-%s).\" % (str(taskid),str(vnodeid),token))\n                    self.add_msg(taskid,username,vnodeid,rpc_pb2.OUTPUTERROR,token,msg)\n                else:\n                    if ret.returncode == 0:\n                        logger.info(\"Task(%s-%s-%s) completed.\" % (str(taskid),str(vnodeid),token))\n                        self.add_msg(taskid,username,vnodeid,rpc_pb2.COMPLETED,token,\"\")\n                    else:\n                        logger.info(\"Task(%s-%s-%s) failed.\" % (str(taskid),str(vnodeid),token))\n                        self.add_msg(taskid,username,vnodeid,rpc_pb2.FAILED,token,\"Runtime Error. More information in stderr log.\")\n\n    def add_msg(self,taskid,username,vnodeid,status,token,errmsg):\n        self.msgslock.acquire()\n        try:\n            self.taskmsgs.append(rpc_pb2.TaskMsg(taskid=str(taskid),username=username,vnodeid=int(vnodeid),subTaskStatus=status,token=token,errmsg=errmsg))\n        except Exception as err:\n            logger.error(traceback.format_exc())\n        self.msgslock.release()\n\n    def report_msg(self):\n        channel = grpc.insecure_channel(self.master_ip+\":\"+self.master_port)\n        stub = rpc_pb2_grpc.MasterStub(channel)\n        while True:\n            self.msgslock.acquire()\n            reportmsg = rpc_pb2.ReportMsg(taskmsgs = self.taskmsgs)\n            try:\n                response = stub.report(reportmsg)\n                logger.info(\"Response from master by reporting: \"+str(response.status)+\" \"+response.message)\n            except Exception as err:\n                logger.error(traceback.format_exc())\n            self.taskmsgs = []\n            self.msgslock.release()\n            time.sleep(self.report_interval)\n\n    def start_report(self):\n        thread = threading.Thread(target = self.report_msg, args=())\n        thread.setDaemon(True)\n        thread.start()\n        logger.info(\"Start to report task messages to master every %d seconds.\" % self.report_interval)\n\ndef TaskWorkerServe():\n    max_threads = int(env.getenv('BATCH_MAX_THREAD_WORKER'))\n    worker_port = int(env.getenv('BATCH_WORKER_PORT'))\n    logger.info(\"Max Threads on a worker is %d\" % max_threads)\n    server = grpc.server(futures.ThreadPoolExecutor(max_workers=max_threads))\n    rpc_pb2_grpc.add_WorkerServicer_to_server(TaskWorker(), server)\n    server.add_insecure_port('[::]:'+str(worker_port))\n    server.start()\n    logger.info(\"Start TaskWorker Servicer on port:%d\" % worker_port)\n    try:\n        while True:\n            time.sleep(_ONE_DAY_IN_SECONDS)\n    except KeyboardInterrupt:\n        server.stop(0)\n\nif __name__ == \"__main__\":\n    TaskWorkerServe()\n"
  },
  {
    "path": "src/worker/worker.py",
    "content": "#!/usr/bin/python3\n\n# first init env\nimport sys\nif sys.path[0].endswith(\"worker\"):\n    sys.path[0] = sys.path[0][:-6]\nfrom utils import env, tools\nconfig = env.getenv(\"CONFIG\")\n#config = \"/opt/docklet/local/docklet-running.conf\"\ntools.loadenv(config)\n\n# must import logger after initlogging, ugly\nfrom utils.log import initlogging\ninitlogging(\"docklet-worker\")\nfrom utils.log import logger\n\nimport xmlrpc.server, sys, time\nfrom socketserver import ThreadingMixIn\nimport threading\nfrom utils import etcdlib, proxytool\nfrom worker import container, monitor\nfrom utils.nettools import netcontrol,ovscontrol,portcontrol\nfrom utils.lvmtool import new_group, recover_group\nfrom master import network\n\n##################################################################\n#                       Worker\n# Description : Worker starts at worker node to listen rpc request and complete the work\n# Init() :\n#      get master ip\n#      initialize rpc server\n#      register rpc functions\n#      initialize network\n#      initialize lvm group\n# Start() :\n#      register in etcd\n#      setup GRE tunnel\n#      start rpc service\n##################################################################\n\n# imitate etcdlib to genernate the key of etcdlib manually\ndef generatekey(path):\n    clustername = env.getenv(\"CLUSTER_NAME\")\n    return '/'+clustername+'/'+path\n\nclass ThreadXMLRPCServer(ThreadingMixIn,xmlrpc.server.SimpleXMLRPCServer):\n    pass\n\nclass Worker(object):\n    def __init__(self, etcdclient, addr, port):\n        self.addr = addr\n        self.port = port\n        logger.info (\"begin initialize on %s\" % self.addr)\n\n        self.fspath = env.getenv('FS_PREFIX')\n        self.poolsize = env.getenv('DISKPOOL_SIZE')\n\n        self.etcd = etcdclient\n        self.master = self.etcd.getkey(\"service/master\")[1]\n        self.mode = None\n        self.workertype = \"normal\"\n        self.key=\"\"\n\n        if len(sys.argv) > 1 and sys.argv[1] == \"batch-worker\":\n            self.workertype = \"batch\"\n\n        if self.workertype == \"normal\":\n            # waiting state is preserved for compatible.\n            self.etcd.setkey(\"machines/runnodes/\"+self.addr, \"waiting\")\n            # get this node's key to judge how to init.\n            [status, key] = self.etcd.getkey(\"machines/runnodes/\"+self.addr)\n            if status:\n                self.key = generatekey(\"machines/allnodes/\"+self.addr)\n            else:\n                logger.error(\"get key failed. %s\" % 'machines/runnodes/'+self.addr)\n                sys.exit(1)\n\n        # check token to check global directory\n        [status, token_1] = self.etcd.getkey(\"token\")\n        tokenfile = open(self.fspath+\"/global/token\", 'r')\n        token_2 = tokenfile.readline().strip()\n        if token_1 != token_2:\n            logger.error(\"check token failed, global directory is not a shared filesystem\")\n            sys.exit(1)\n        logger.info (\"worker registered and checked the token\")\n\n        # worker search all run nodes to judge how to init\n        # If the node in all node list, we will recover it.\n        # Otherwise, this node is new added in.\n        value = 'init-new'\n        [status, alllist] = self.etcd.listdir(\"machines/allnodes\")\n        for node in alllist:\n            if node['key'] == self.key:\n                value = 'init-recovery'\n                break\n\n        logger.info(\"worker start in \"+value+\" mode, worker type is\"+self.workertype)\n\n        Containers = container.Container(self.addr, etcdclient)\n        if value == 'init-new':\n            logger.info (\"init worker with mode:new\")\n            self.mode='new'\n            # check global directory do not have containers on this worker\n            [both, onlylocal, onlyglobal] = Containers.diff_containers()\n            if len(both+onlyglobal) > 0:\n                logger.error (\"mode:new will clean containers recorded in global, please check\")\n                sys.exit(1)\n            [status, info] = Containers.delete_allcontainers()\n            if not status:\n                logger.error (\"delete all containers failed\")\n                sys.exit(1)\n            # create new lvm VG at last\n            new_group(\"docklet-group\",self.poolsize,self.fspath+\"/local/docklet-storage\")\n            #subprocess.call([self.libpath+\"/lvmtool.sh\", \"new\", \"group\", \"docklet-group\", self.poolsize, self.fspath+\"/local/docklet-storage\"])\n        elif value == 'init-recovery':\n            logger.info (\"init worker with mode:recovery\")\n            self.mode='recovery'\n            # recover lvm VG first\n            recover_group(\"docklet-group\",self.fspath+\"/local/docklet-storage\")\n            #subprocess.call([self.libpath+\"/lvmtool.sh\", \"recover\", \"group\", \"docklet-group\", self.fspath+\"/local/docklet-storage\"])\n            [status, meg] = Containers.check_allcontainers()\n            if status:\n                logger.info (\"all containers check ok\")\n            else:\n                logger.info (\"not all containers check ok\")\n                #sys.exit(1)\n        else:\n            logger.error (\"worker init mode:%s not supported\" % value)\n            sys.exit(1)\n        # init portcontrol\n        logger.info(\"init portcontrol\")\n        portcontrol.init_new()\n\n        # initialize rpc\n        # xmlrpc.server.SimpleXMLRPCServer(addr) -- addr : (ip-addr, port)\n        # if ip-addr is \"\", it will listen ports of all IPs of this host\n        logger.info (\"initialize rpcserver %s:%d\" % (self.addr, int(self.port)))\n        # logRequests=False : not print rpc log\n        #self.rpcserver = xmlrpc.server.SimpleXMLRPCServer((self.addr, self.port), logRequests=False)\n        self.rpcserver = ThreadXMLRPCServer((self.addr, int(self.port)), allow_none=True, logRequests=False)\n        self.rpcserver.register_introspection_functions()\n        self.rpcserver.register_instance(Containers)\n        self.rpcserver.register_function(monitor.workerFetchInfo)\n        self.rpcserver.register_function(netcontrol.setup_gw)\n        self.rpcserver.register_function(netcontrol.del_gw)\n        self.rpcserver.register_function(netcontrol.del_bridge)\n        self.rpcserver.register_function(ovscontrol.add_port_gre_withkey)\n        self.rpcserver.register_function(netcontrol.check_gw)\n        self.rpcserver.register_function(netcontrol.recover_usernet)\n        self.rpcserver.register_function(proxytool.set_route)\n        self.rpcserver.register_function(proxytool.delete_route)\n        self.rpcserver.register_function(portcontrol.acquire_port_mapping)\n        self.rpcserver.register_function(portcontrol.release_port_mapping)\n        # register functions or instances to server for rpc\n        #self.rpcserver.register_function(function_name)\n\n        # init collector to collect monitor infomation\n        self.con_collector = monitor.Container_Collector()\n        self.hosts_collector = monitor.Collector()\n\n        # delete the existing network\n        #[success, bridges] = ovscontrol.list_bridges()\n        #if success:\n        #    for bridge in bridges:\n        #        if bridge.startswith(\"docklet-br\"):\n        #            ovscontrol.del_bridge(bridge)\n        #else:\n        #    logger.error(bridges)\n        #[success, message] = ovscontrol.destroy_all_qos()\n        #if not success:\n        #    logger.error(message)\n        '''if (self.addr == self.master):\n            logger.info (\"master also on this node. reuse master's network\")\n        else:\n            logger.info (\"initialize network\")\n            # 'docklet-br' of worker do not need IP Addr.\n            #[status, result] = self.etcd.getkey(\"network/workbridge\")\n            #if not status:\n            #    logger.error (\"get bridge IP failed, please check whether master set bridge IP for worker\")\n            #self.bridgeip = result\n            # create bridges for worker\n            #network.netsetup(\"init\", self.bridgeip)\n            if self.mode == 'new':\n                if netcontrol.bridge_exists('docklet-br'):\n                    netcontrol.del_bridge('docklet-br')\n                netcontrol.new_bridge('docklet-br')\n            else:\n                if not netcontrol.bridge_exists('docklet-br'):\n          utils       logger.error(\"docklet-br not found\")\n                    sys.exit(1)\n            logger.info (\"setup GRE tunnel to master %s\" % self.master)\n            #network.netsetup(\"gre\", self.master)\n            #if not netcontrol.gre_exists('docklet-br', self.master):\n                #netcontrol.setup_gre('docklet-br', self.master)'''\n\n    # start service of worker\n    def start(self):\n        # start collector\n        self.con_collector.start()\n        self.hosts_collector.start()\n        logger.info(\"Monitor Collector has been started.\")\n        # worker change it state itself. Independedntly from master.\n        if self.workertype == \"normal\":\n            self.etcd.setkey(\"machines/runnodes/\"+self.addr, \"work\")\n        publicIP = env.getenv(\"PUBLIC_IP\")\n        self.etcd.setkey(\"machines/publicIP/\"+self.addr,publicIP)\n        self.thread_sendheartbeat = threading.Thread(target=self.sendheartbeat)\n        self.thread_sendheartbeat.start()\n        # start serving for rpc\n        logger.info (\"begins to work\")\n        self.rpcserver.serve_forever()\n\n    # send heardbeat package to keep alive in etcd, ttl=2s\n    def sendheartbeat(self):\n        if self.workertype == \"normal\":\n            while(True):\n                # check send heartbeat package every 1s\n                time.sleep(2)\n                [status, value] = self.etcd.getkey(\"machines/runnodes/\"+self.addr)\n                if status:\n                    # master has know the worker so we start send heartbeat package\n                    if value=='ok':\n                        self.etcd.setkey(\"machines/runnodes/\"+self.addr, \"ok\", ttl = 60)\n                else:\n                    logger.error(\"get key %s failed, master may be crashed\" % self.addr)\n                    self.etcd.setkey(\"machines/runnodes/\"+self.addr, \"ok\", ttl = 60)\n        elif self.workertype == \"batch\":\n            while(True):\n                time.sleep(2)\n                self.etcd.setkey(\"machines/batchnodes/\"+self.addr, \"ok\", ttl = 60)\n\n\nif __name__ == '__main__':\n\n    etcdaddr = env.getenv(\"ETCD\")\n    logger.info (\"using ETCD %s\" % etcdaddr )\n\n    clustername = env.getenv(\"CLUSTER_NAME\")\n    logger.info (\"using CLUSTER_NAME %s\" % clustername )\n\n    # get network interface\n    net_dev = env.getenv(\"NETWORK_DEVICE\")\n    logger.info (\"using NETWORK_DEVICE %s\" % net_dev )\n\n    ipaddr = network.getip(net_dev)\n    if ipaddr is False:\n        logger.error(\"network device is not correct\")\n        sys.exit(1)\n    else:\n        logger.info(\"using ipaddr %s\" % ipaddr)\n    # init etcdlib client\n    try:\n        etcdclient = etcdlib.Client(etcdaddr, prefix = clustername)\n    except Exception:\n        logger.error (\"connect etcd failed, maybe etcd address not correct...\")\n        sys.exit(1)\n    else:\n        logger.info(\"etcd connected\")\n\n    cpu_quota = env.getenv('CONTAINER_CPU')\n    logger.info (\"using CONTAINER_CPU %s\" % cpu_quota )\n\n    mem_quota = env.getenv('CONTAINER_MEMORY')\n    logger.info (\"using CONTAINER_MEMORY %s\" % mem_quota )\n\n    worker_port = env.getenv('WORKER_PORT')\n    logger.info (\"using WORKER_PORT %s\" % worker_port )\n\n\n    logger.info(\"Starting worker\")\n    worker = Worker(etcdclient, addr=ipaddr, port=worker_port)\n    worker.start()\n"
  },
  {
    "path": "tools/DOCKLET_NOTES.txt",
    "content": "** MUST READ **\n\n1. Please keep your important data in ~/nfs directory. It will not be\ndestroyed even if the workspace is deleted. \n\n2. If you delete your workspace, all data in your Home directory will \nbe lost, except those in ~/nfs directory.\n\n3. You can save your workspace as a private image if you have modified \nthe system and do not want to repeat it in your new workspace or new\ncontainer.\n\n4. Your containers are distributed by default. So it is ideal for simple\nparallel jobs.\n\n5. If you find the Web Terminal not align correctly, choose a monospace\nfont may help.\n"
  },
  {
    "path": "tools/R_demo.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 一个R语言实现的爬虫，爬取拉手网美食信息\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"点击下边的cell，点击上方工具栏里的执行图标，即可执行代码块，看到输出结果。代码块左边的In[]出现In[*]表示代码正在执行\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"library(XML)\\n\",\n    \"\\n\",\n    \"giveNames = function(rootNode){\\n\",\n    \"  names <- xpathSApply(rootNode,\\\"//h3/a[@class='goods-name']\\\",xmlValue)\\n\",\n    \"  names\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"givesevices = function(rootNode){\\n\",\n    \"  sevices <- xpathSApply(rootNode,\\\"//h3/a[@class='goods-text']\\\",xmlValue)\\n\",\n    \"  sevices\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"giveprices = function(rootNode){\\n\",\n    \"  prices <- xpathSApply(rootNode,\\\"//div/span[@class='price']\\\",xmlValue)\\n\",\n    \"  prices\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"givemoney = function(rootNode){\\n\",\n    \"  money <- xpathSApply(rootNode,\\\"//div/span[@class='money']\\\",xmlValue)\\n\",\n    \"  money\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"giveplaces = function(rootNode){\\n\",\n    \"  places <- xpathSApply(rootNode,\\\"//a/span[@class='goods-place']\\\",xmlValue)\\n\",\n    \"  places\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"getmeituan = function(URL){\\n\",\n    \"  Sys.sleep(runif(1,1,2))\\n\",\n    \"  doc<-htmlParse(URL[1],encoding=\\\"UTF-8\\\")\\n\",\n    \"  rootNode<-xmlRoot(doc)\\n\",\n    \"  data.frame(\\n\",\n    \"    Names=giveNames(rootNode), #店名\\n\",\n    \"    services=givesevices(rootNode), #服务\\n\",\n    \"    prices=giveprices(rootNode),  #现价\\n\",\n    \"    money=givemoney(rootNode),  #原价\\n\",\n    \"    places=giveplaces(rootNode)  #地点\\n\",\n    \"    \\n\",\n    \"  )\\n\",\n    \"}\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"URL = paste0(\\\"http://shenzhen.lashou.com/cate/meishi/page\\\",1:10)\\n\",\n    \"\\n\",\n    \"mainfunction = function(URL){\\n\",\n    \"  data = rbind(\\n\",\n    \"    getmeituan (URL[1]),\\n\",\n    \"    getmeituan (URL[2]),\\n\",\n    \"    getmeituan (URL[3]),\\n\",\n    \"    getmeituan (URL[4]),\\n\",\n    \"    getmeituan (URL[5])\\n\",\n    \"  )\\n\",\n    \"  \\n\",\n    \"  \\n\",\n    \"}\\n\",\n    \"ll=mainfunction(URL)\\n\",\n    \"write.table(ll,\\\"result.txt\\\",row.names=FALSE)\\n\",\n    \"ll\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# R语言的线性回归实例\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"输入数据\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"#读入数据\\n\",\n    \"x<- c(0.10,0.11,0.12,0.13,0.14,0.15,0.16,0.17,0.18,0.20,0.21,0.23)\\n\",\n    \"y<-c(42.0,43.5,45.0,45.5,45.0,47.5,49,53,50,55,55,60)\\n\",\n    \"#绘出 x 与 y 的散列图\\n\",\n    \"plot(y~x)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"执行该段代码，即可看到输出图形；从图中我们可以看出 y 和 x 存在线性相关性，可以进行线性回归分析：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"model<-lm(y~x)\\n\",\n    \"summary(model)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"我们通过 P 值(就是上面的 pr 那一列)来查看对应的解释变量 x 的显著性，通过将 p 值与 0.05 进行比较，若改值小于 0.05，就可以说该变量与被解释变量存在显著的相关性。\\n\",\n    \"\\n\",\n    \"Multiple R-squared 和 Adjusted R-squared 这两个值，就是我们常称为”拟合优度“和”修正的拟合优度“，是指回归方程对样本的拟合程度，这里我们可以看到，修正的拟合优度为 0.9429，表示拟合程度超过五成，这个值越高越好。\\n\",\n    \"\\n\",\n    \"最后，看下 F-statistic，也就是常说的 F 统计量，也称为 F 检验，常用语判断方程整体的显著性实验，其 p 值为 9.505e-08，显然小于 0.05，我们可以认为方程在 P=0.05 的水平上是通过显著性检验的。\\n\",\n    \"\\n\",\n    \"从上面我们看出我们的线性回归效果不错，那么我们可以利用拟合方程进行分类，或者预测。\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"newX<-data.frame(x=0.16)\\n\",\n    \"predict(model,newdata=newX,interval=\\\"prediction\\\",level=0.95)#interval=”prediction“ level指定预测的置信区间\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# R语言的逻辑回归示例\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"counts <- c(18,17,15,20,10,20,25,13,12)\\n\",\n    \"outcome <- gl(3,1,9)\\n\",\n    \"treatment <- gl(3,3)\\n\",\n    \"print(d.AD <- data.frame(treatment, outcome, counts))\\n\",\n    \"glm.D93 <- glm(counts ~ outcome + treatment, family = poisson())\\n\",\n    \"anova(glm.D93)\\n\",\n    \"summary(glm.D93)\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": []\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"R\",\n   \"language\": \"R\",\n   \"name\": \"ir\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": \"r\",\n   \"file_extension\": \".r\",\n   \"mimetype\": \"text/x-r-source\",\n   \"name\": \"R\",\n   \"pygments_lexer\": \"r\",\n   \"version\": \"3.2.3\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "tools/alterUserTable.py",
    "content": "import sys\nsys.path.append(\"../src/\")\nfrom model import db,User\n\nprint(\"Query all users:\")\nusers = User.query.all()\nprint(users)\nnewusers = []\nprint(\"Copy data to new users and set their beans to 10000...\")\nfor user in users:\n    newuser = User(user.username,user.password,user.avatar,user.nickname,user.description,user.status,\n                    user.e_mail,user.student_number,user.department,user.truename,user.tel,user.register_date,\n                    user.user_group,user.auth_method)\n    newuser.beans = 10000\n    newusers.append(newuser)\nprint(\"Drop all table...\")\ndb.drop_all(bind='__all__')\nprint(\"Create all tables with beans...\")\nsetattr(User,'beans',db.Column(db.Integer))\ndb.create_all(bind='__all__')\nfor newuser in newusers:\n    db.session.add(newuser)\n    db.session.commit()\nprint(\"Update users table successfully!\")\n"
  },
  {
    "path": "tools/clean-usage.py",
    "content": "#!/usr/bin/python3\n\nimport os, json, sys\nsys.path.append(\"../src/\")\nfrom model import db, User, UserUsage\n\ndef clean_usage(username,alluser=False):\n    if alluser:\n        usages = UserUsage.query.all()\n        for usage in usages:\n            usage.cpu = str(0)\n            usage.memory = str(0)\n            usage.disk = str(0)\n        db.session.commit()\n    else:\n        usage = UserUsage.query.filter_by(username = username).first()\n        usage.cpu = str(0)\n        usage.memory = str(0)\n        usage.disk = str(0)        \n        db.session.commit()\n    return \n\nif __name__ == '__main__':\n    if len(sys.argv) >= 2:\n        username = sys.argv[1]\n        clean_usage(username)\n    else:\n        clean_usage(\"user\",True)\n"
  },
  {
    "path": "tools/cloudsetting.aliyun.template.json",
    "content": "{\n\t\"CloudName\": \"aliyun\", \n\t\"AccessKeyId\": \"your-key\", \n\t\"AccessKeySecret\": \"your-secret\", \n\t\"RegionId\": \"cn-beijing\", \t\n\t\"ZoneId\": \"cn-beijing-a\", \n\t\"InstanceType\": \"ecs.sn1ne.xlarge\",\n\t\"SystemDisk.Size\": 500,\n\t\"Password\": \"Unias1234\",\n\t\"VSwitchId\": \"the vswitchid of your vpc\",\n\t\"VolumeName\": \"the volume name (with host name) of your glusterfs\"\n}\n"
  },
  {
    "path": "tools/dl_start_spark.sh",
    "content": "#!/bin/sh\n\n# a naive script to fast start spark cluster, assuming host-0 master,\n# others slaves.\n# used with dl_stop_spark.sh\n\nSPARK_HOME=/home/spark\n\nHOSTS=`grep -v localhost /etc/hosts | awk '{print $2}'`\n\necho \"Starting master in host-0\"\n\n$SPARK_HOME/sbin/start-master.sh\n\nfor h in $HOSTS ; do\n    echo \"Starting slave in $h\"\n    if [ $h  != 'host-0' ] ; then\n        ssh root@$h /home/spark/sbin/start-slave.sh spark://host-0:7077\n    else\n        /home/spark/sbin/start-slave.sh spark://host-0:7077\n    fi\ndone\n"
  },
  {
    "path": "tools/dl_stop_spark.sh",
    "content": "#!/bin/sh\n\n# a naive script to stop spark cluster, assuming host-0 master\n# others slaves\n# used with dl_start_spark.sh\n\nSPARK_HOME=/home/spark\n\nHOSTS=`grep -v localhost /etc/hosts | awk '{print $2}'`\n\nfor h in $HOSTS ; do\n    echo \"Stopping slave in $h\"\n    if [ $h  != 'host-0' ] ; then\n        ssh root@$h /home/spark/sbin/stop-slave.sh\n    else\n        /home/spark/sbin/stop-slave.sh \n    fi\ndone\n\necho \"Stopping master in host-0\"\n\n$SPARK_HOME/sbin/stop-master.sh\n\n"
  },
  {
    "path": "tools/docklet-deploy.sh",
    "content": "apt-get update\n\napt-get install -y git\n\ngit clone http://github.com/unias/docklet.git /home/docklet\n\n/home/docklet/prepare.sh\n\ncp /home/docklet/conf/docklet.conf.template /home/docklet/conf/docklet.conf\ncp /home/docklet/web/templates/home.template /home/docklet/web/templates/home.html\n\nNETWORK_DEVICE=`route | grep default | awk {'print $8'};`\n\necho \"DISKPOOL_SIZE=200000\nETCD=%MASTERIP%:2379\nNETWORK_DEVICE=$NETWORK_DEVICE\nPROXY_PORT=8000\nNGINX_PORT=80\" >> /home/docklet/conf/docklet.conf\n\n#please modify the mount command for your corresponding distributed file system if you are not using glusterfs\nmount -t glusterfs %VOLUMENAME% /opt/docklet/global/\n\nif [ -f /opt/docklet/global/packagefs.tgz ]; then\n\ttar zxvf /opt/docklet/global/packagefs.tgz -C /opt/docklet/local/ > /dev/null\nfi\n\n/home/docklet/bin/docklet-worker start\nexit 0\n"
  },
  {
    "path": "tools/etcd-multi-nodes.sh",
    "content": "#!/bin/bash\n\n# more details for https://coreos.com/etcd/docs/latest\n\nwhich etcd &>/dev/null || { echo \"etcd not installed, please install etcd first\" && exit 1; }\n\nif [ $# -eq 0 ] ; then\n    echo \"Usage: `basename $0` index selfip ip1 ip2 ip3\"\n    echo \"    ip1 ip2 ip3 are the ip address of node etcd_1 etcd_2 etcd_3\"\n    exit 1\nfi\n\netcdindex=etcd_$1\nshift\nselfip=$1\nshift\netcd_1=$1\nindex=1\nwhile [ $# -gt 0 ] ; do\n    h=\"etcd_$index\"\n    if [ $index -eq 1 ] ; then\n        CLUSTER=\"$h=http://$1:2380\"\n    else\n        CLUSTER=\"$CLUSTER,$h=http://$1:2380\"\n    fi\n    index=$(($index+1))\n    shift\ndone\n\n# -initial-advertise-peer-urls  :  tell others what peer urls of me\n# -listen-peer-urls             :  what peer urls of me\n\n# -listen-client-urls           :  what client urls to listen\n# -advertise-client-urls        :  tell others what client urls to listen of me\n\n# -initial-cluster-state        :  new means join a new cluster; existing means join an existing cluster\n#                               :  new not means clear\n\ndepdir=${0%/*}\ntempdir=/opt/docklet/local\n[ ! -d $tempdir/log ] && mkdir -p $tempdir/log\n[ ! -d $tempdir/run ] && mkdir -p $tempdir/run\n\netcd --name $etcdindex \\\n     --initial-advertise-peer-urls http://$selfip:2380 \\\n     --listen-peer-urls http://$selfip:2380 \\\n     --listen-client-urls http://$selfip:2379,http://127.0.0.1:2379 \\\n     --advertise-client-urls http://$selfip:2379 \\\n     --initial-cluster-token etcd-cluster \\\n     --initial-cluster $CLUSTER \\\n     --initial-cluster-state new > $tempdir/log/etcd.log 2>&1 &\n\netcdpid=$!\necho \"etcd start with pid: $etcdpid and log:$tempdir/log/etcd.log\"\necho $etcdpid > $tempdir/run/etcd.pid\n"
  },
  {
    "path": "tools/etcd-one-node.sh",
    "content": "#!/bin/sh\n\n# more details for https://coreos.com/etcd/docs/latest\n\n#which etcd &>/dev/null || { echo \"etcd not installed, please install etcd first\" && exit 1; }\nwhich etcd >/dev/null || { echo \"etcd not installed, please install etcd first\" && exit 1; }\n\netcd_1=localhost\n\nif [ $# -gt 0 ] ; then\n    etcd_1=$1\nfi\n\n\n# -initial-advertise-peer-urls  :  tell others what peer urls of me\n# -listen-peer-urls             :  what peer urls of me\n\n# -listen-client-urls           :  what client urls to listen\n# -advertise-client-urls        :  tell others what client urls to listen of me\n\n# -initial-cluster-state        :  new means join a new cluster; existing means a new node join an existing cluster\n#                               :  new not means clear, old data is still alive\n\ndepdir=${0%/*}\ntempdir=/opt/docklet/local\n[ ! -d $tempdir/log ] && mkdir -p $tempdir/log\n[ ! -d $tempdir/run ] && mkdir -p $tempdir/run\n\necho \"starting etcd on $etcd_1\"\n\n#stdbuf -o0 -e0 $tempdir/etcd --name etcd_1 \\\netcd --name etcd_1 \\\n\t --data-dir $tempdir/etcd_data \\\n     --initial-advertise-peer-urls http://$etcd_1:2380 \\\n     --listen-peer-urls http://$etcd_1:2380 \\\n     --listen-client-urls http://$etcd_1:2379 \\\n     --advertise-client-urls http://$etcd_1:2379 \\\n     --initial-cluster-token etcd_cluster \\\n     --initial-cluster etcd_1=http://$etcd_1:2380 \\\n     --initial-cluster-state new > $tempdir/log/etcd.log 2>&1 &\n\netcdpid=$!\necho \"etcd start with pid: $etcdpid and log:$tempdir/log/etcd.log\"\necho $etcdpid > $tempdir/run/etcd.pid\n\n"
  },
  {
    "path": "tools/nginx_config.sh",
    "content": "#!/bin/sh\n\nMASTER_IP=0.0.0.0\nNGINX_PORT=8080\nPROXY_PORT=8000\nWEB_PORT=8888\nNGINX_CONF=/etc/nginx\n\ntoolsdir=${0%/*}\nDOCKLET_TOOLS=$(cd $toolsdir; pwd)\nDOCKLET_HOME=${DOCKLET_TOOLS%/*}\nDOCKLET_CONF=$DOCKLET_HOME/conf\n\n. $DOCKLET_CONF/docklet.conf\n\nNGINX_CONF=${NGINX_CONF}/sites-enabled\n\necho \"copy nginx_docklet.conf to nginx config path...\"\ncp $DOCKLET_CONF/nginx_docklet.conf ${NGINX_CONF}/\nsed -i \"s/%MASTER_IP/${MASTER_IP}/g\" ${NGINX_CONF}/nginx_docklet.conf\nsed -i \"s/%NGINX_PORT/${NGINX_PORT}/g\" ${NGINX_CONF}/nginx_docklet.conf\n\nsed -i \"s/%PROXY_PORT/${PROXY_PORT}/g\" ${NGINX_CONF}/nginx_docklet.conf\nsed -i \"s/%WEB_PORT/${WEB_PORT}/g\" ${NGINX_CONF}/nginx_docklet.conf\n\nif [ \"${NGINX_PORT}\" != \"80\" ] && [ \"${NGINX_PORT}\" != \"443\" ]\nthen\n  sed -i \"s/\\$host/\\$host:\\$server_port/g\" ${NGINX_CONF}/nginx_docklet.conf\nfi\n\n\n\necho \"restart nginx...\"\n/etc/init.d/nginx restart\n"
  },
  {
    "path": "tools/npmrc",
    "content": "registry = https://registry.npm.taobao.org\n"
  },
  {
    "path": "tools/pip.conf",
    "content": "[global]\nindex-url=https://pypi.mirrors.ustc.edu.cn/simple/\n"
  },
  {
    "path": "tools/python_demo.ipynb",
    "content": "{\n \"cells\": [\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"# 用Python分析《美女与野兽》\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"在一篇最近发表的论文*A quantitative analysis of gendered compliments in Disney Princess films*中，Carmen Fought和Karen Eisenhauer发现在这部迪士尼经典影片中女性角色的对话要多于迪士尼近期的电影作品。作者在网络上发现了美女与《野兽》的脚本，因此我立刻用Python重做了他们的分析。\\n\",\n    \"<br />更多地，我在文章最后加入了对《玩具总动员》的分析，这个脚本的形式完全不同，但其中91%的对白来自男性角色。\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"点击下边的cell，点击上方工具栏里的执行图标，即可执行代码块，看到输出结果。代码块左边的In[]出现In[*]表示代码正在执行\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"from __future__ import division\\n\",\n    \"\\n\",\n    \"import re\\n\",\n    \"from collections import defaultdict\\n\",\n    \"\\n\",\n    \"import requests\\n\",\n    \"import pandas as pd\\n\",\n    \"import matplotlib\\n\",\n    \"\\n\",\n    \"%matplotlib inline\\n\",\n    \"matplotlib.style.use('ggplot')\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Load the script which comes as a text file\\n\",\n    \"\\n\",\n    \"script_url = 'http://www.fpx.de/fp/Disney/Scripts/BeautyAndTheBeast.txt'\\n\",\n    \"script = requests.get(script_url).text\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"我们看下脚本的开篇：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Let's look at the beginning of the script\\n\",\n    \"\\n\",\n    \"script.splitlines()[:20]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"再在中间随意选取一段：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Let's look at a random place\\n\",\n    \"\\n\",\n    \"script.splitlines()[500:520]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"看上去很容易分析，因为角色和对白间用:隔开\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# seems fairly easy to parse since \\n\",\n    \"# each new speaking line has : and begins with all caps\\n\",\n    \"\\n\",\n    \"def remove_spaces(line):\\n\",\n    \"    # remove the weird spaces\\n\",\n    \"    return re.sub(' +',' ',line)\\n\",\n    \"\\n\",\n    \"def remove_paren(line):\\n\",\n    \"    # remove directions that are not spoken\\n\",\n    \"    return re.sub(r'\\\\([^)]*\\\\)', '', line)\\n\",\n    \"\\n\",\n    \"\\n\",\n    \"lines = []\\n\",\n    \"line = ''\\n\",\n    \"for row in script.splitlines():\\n\",\n    \"    if ': ' in row and row[:3].upper() == row[:3]:\\n\",\n    \"        line = remove_spaces(line)\\n\",\n    \"        line = remove_paren(line)\\n\",\n    \"        lines.append(line)\\n\",\n    \"        line = row\\n\",\n    \"    elif '          ' in row:\\n\",\n    \"        line = line + ' ' + row.lstrip()\\n\",\n    \"# don't forget the last line\\n\",\n    \"lines.append(remove_spaces(line))\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"lines[:15]\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"看看结尾什么样：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# How does the end look\\n\",\n    \"\\n\",\n    \"lines[-5:]\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# 我们去掉可能的空白行\\n\",\n    \"\\n\",\n    \"print (len(lines))\\n\",\n    \"lines = [l for l in lines if len(l) > 0]\\n\",\n    \"print (len(lines))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"现在，我们找出所有角色，并计算他们的出场次数（对白数）\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# now figure out the roles and how many times they appear\\n\",\n    \"\\n\",\n    \"roles = defaultdict(int)\\n\",\n    \"\\n\",\n    \"for line in lines:\\n\",\n    \"    # take advantage of the fact that the speaker is always listed before the :\\n\",\n    \"    speaker = line.split(':')[0]\\n\",\n    \"    roles[speaker] = roles[speaker] + 1\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"len(roles)\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"看一下每个角色出现的相对频率：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# take a look at the relative frequency of each role\\n\",\n    \"roles\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"看起来有一行“to think about”是乱入的（恰好满足了parse条件），我们忽略它\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Looks like there is one bum line ('to think about'')\\n\",\n    \"# But I'll ignore that for now.\\n\",\n    \"\\n\",\n    \"# Quickly eye ball which roles are female and which are possibly mixed groups.\\n\",\n    \"\\n\",\n    \"females = ['WOMAN 1',\\n\",\n    \"           'WOMAN 2',\\n\",\n    \"           'WOMAN 3',\\n\",\n    \"           'WOMAN 4',\\n\",\n    \"           'WOMAN 5',\\n\",\n    \"           'OLD CRONIES',\\n\",\n    \"           'MRS. POTTS',\\n\",\n    \"           'BELLE',\\n\",\n    \"           'BIMBETTE 1'\\n\",\n    \"           'BIMBETTE 2',\\n\",\n    \"           'BIMBETTE 3']\\n\",\n    \"\\n\",\n    \"groups = ['MOB',\\n\",\n    \"          'ALL',\\n\",\n    \"          'BOTH']\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"将每一行对白根据角色性别进行标记，并统计不同性别的对白数量\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Mark each line of dialogue by sex and count them\\n\",\n    \"\\n\",\n    \"sex_lines = {'Male':   0,\\n\",\n    \"             'Female': 0}\\n\",\n    \"\\n\",\n    \"for line in lines:\\n\",\n    \"    # Extract speaker \\n\",\n    \"    speaker = line.split(':')[0]\\n\",\n    \"    \\n\",\n    \"    if speaker in females:\\n\",\n    \"        sex_lines['Female'] += 1\\n\",\n    \"        \\n\",\n    \"    elif sex_lines not in groups:\\n\",\n    \"        sex_lines['Male'] += 1\\n\",\n    \"\\n\",\n    \"print (sex_lines)\\n\",\n    \"print (sex_lines['Male']/(sex_lines['Male'] + sex_lines['Female']))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"我们使用一张图来显示结果：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Quick graphical representation \\n\",\n    \"\\n\",\n    \"df = pd.DataFrame([sex_lines.values()],columns=sex_lines.keys())\\n\",\n    \"df.plot(kind='bar')\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"也许男性角色和女性角色的对白长度有明显不同？我们来看一看<br/>这次我们计算对白中单词数量而不是计算对白次数：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Maybe men and women talk for different lengths? This counts words instead of \\n\",\n    \"\\n\",\n    \"sex_words = {'Male':   0,\\n\",\n    \"             'Female': 0}\\n\",\n    \"\\n\",\n    \"for line in lines:\\n\",\n    \"    speaker = line.split(':')[0]\\n\",\n    \"    dialogue = line.split(':')[1]  \\n\",\n    \"    # remove the \\n\",\n    \"    # tokenize sentence by spaces\\n\",\n    \"    word_count = len(dialogue.split(' ')) \\n\",\n    \"                    \\n\",\n    \"    if speaker in females:\\n\",\n    \"        sex_words['Female'] += word_count\\n\",\n    \"    elif speaker not in groups:\\n\",\n    \"        sex_words['Male'] += word_count\\n\",\n    \"\\n\",\n    \"print (sex_words)\\n\",\n    \"print (sex_words['Male']/(sex_words['Male'] + sex_words['Female']))\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"也用图表显示出来：\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Quick graphical representation \\n\",\n    \"\\n\",\n    \"df = pd.DataFrame([sex_words.values()],columns=sex_words.keys())\\n\",\n    \"df.plot(kind='bar')\"\n   ]\n  },\n  {\n   \"cell_type\": \"markdown\",\n   \"metadata\": {},\n   \"source\": [\n    \"下面是额外的《玩具总动员》的分析\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": false\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"# Bonus toy story analysis\\n\",\n    \"\\n\",\n    \"url = 'http://www.dailyscript.com/scripts/toy_story.html'\\n\",\n    \"toy_story_script = requests.get(url).text\\n\",\n    \"\\n\",\n    \"# toy_story_script.splitlines()[250:350]\\n\",\n    \"\\n\",\n    \"lines = []\\n\",\n    \"speaker = ''\\n\",\n    \"dialogue = ''\\n\",\n    \"for row in toy_story_script.splitlines()[90:]:\\n\",\n    \"    if '                     ' in row: \\n\",\n    \"        if ':' not in speaker:\\n\",\n    \"            lines.append( {'Speaker': remove_paren(speaker).strip(),\\n\",\n    \"                           'Dialogue': remove_paren(dialogue).strip() } )\\n\",\n    \"        \\n\",\n    \"        speaker = remove_spaces(row.strip())\\n\",\n    \"        dialogue = ''\\n\",\n    \"    elif '            ' in row:\\n\",\n    \"        dialogue = dialogue + ' ' + remove_spaces(row)\\n\",\n    \"lines.append( {'Speaker': remove_paren(speaker).strip(),\\n\",\n    \"               'Dialogue': remove_paren(dialogue).strip() } )\\n\",\n    \"\\n\",\n    \"roles = defaultdict(int)\\n\",\n    \"\\n\",\n    \"for line in lines:\\n\",\n    \"    speaker = line['Speaker']\\n\",\n    \"    roles[speaker] = roles[speaker] + 1\\n\",\n    \"\\n\",\n    \"toy_story_df = pd.DataFrame(lines[1:])\\n\",\n    \"toy_story_df.head()\\n\",\n    \"\\n\",\n    \"toy_story_df.Speaker.value_counts()\\n\",\n    \"\\n\",\n    \"def what_sex(speaker):\\n\",\n    \"    if speaker in [\\\"SID'S MOM\\\", 'MRS. DAVIS', 'HANNAH', 'BO PEEP']:\\n\",\n    \"        return 'Female'\\n\",\n    \"    return 'Male'\\n\",\n    \"\\n\",\n    \"toy_story_df['Sex'] = toy_story_df['Speaker'].apply(what_sex)\\n\",\n    \"\\n\",\n    \"sex_df = toy_story_df.groupby('Sex').size()\\n\",\n    \"sex_df.plot(kind='bar')\\n\",\n    \"sex_df\\n\",\n    \"\\n\"\n   ]\n  },\n  {\n   \"cell_type\": \"code\",\n   \"execution_count\": null,\n   \"metadata\": {\n    \"collapsed\": true\n   },\n   \"outputs\": [],\n   \"source\": [\n    \"def word_count(dialogue):\\n\",\n    \"    return len(dialogue.split())\\n\",\n    \"\\n\",\n    \"toy_story_df['Word Count'] = toy_story_df['Dialogue'].apply(word_count)\\n\",\n    \"\\n\",\n    \"word_df = toy_story_df.groupby('Sex')['Word Count'].sum()\\n\",\n    \"word_df.plot(kind='bar')\\n\",\n    \"word_df\"\n   ]\n  }\n ],\n \"metadata\": {\n  \"kernelspec\": {\n   \"display_name\": \"Python 3\",\n   \"language\": \"python\",\n   \"name\": \"python3\"\n  },\n  \"language_info\": {\n   \"codemirror_mode\": {\n    \"name\": \"ipython\",\n    \"version\": 3\n   },\n   \"file_extension\": \".py\",\n   \"mimetype\": \"text/x-python\",\n   \"name\": \"python\",\n   \"nbconvert_exporter\": \"python\",\n   \"pygments_lexer\": \"ipython3\",\n   \"version\": \"3.5.1+\"\n  }\n },\n \"nbformat\": 4,\n \"nbformat_minor\": 0\n}\n"
  },
  {
    "path": "tools/resolv.conf",
    "content": "nameserver 162.105.129.26\nnameserver 162.105.129.27\n"
  },
  {
    "path": "tools/sources.list",
    "content": "deb https://mirrors.ustc.edu.cn/ubuntu/ xenial main restricted universe multiverse\n"
  },
  {
    "path": "tools/start_jupyter.sh",
    "content": "#!/bin/sh \n\n#\n# this script should be placed in basefs/home/jupyter\n# \n\n# This next line determines what user the script runs as.\nDAEMON_USER=root\n\n# settings for docklet worker\nDAEMON=`which jupyterhub-singleuser`\nDAEMON_NAME=jupyter\n# The process ID of the script when it runs is stored here:\nPIDFILE=/home/jupyter/$DAEMON_NAME.pid\n\nRUN_DIR=/root\n\n#export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games\n\n#export HOME=/home\n\n#export SHELL=/bin/bash\n\n#export LOGNAME=root\n\n# JPY_API_TOKEN is needed by jupyterhub-singleuser\n# it will send this token in request header to hub-api-url for authorization\n# but we don't use this by now\nexport JPY_API_TOKEN=not-use\n\n# user for this notebook\nUSER=root\n# port to start service\nPORT=10000\n# cookie name to get from http request and send to hub_api_url for authorization\nCOOKIE_NAME=docklet-jupyter-cookie\n# base url of this server. client will use this url for request\nBASE_URL=/workspace/$USER\n# prefix for login and logout\nHUB_PREFIX=/jupyter\n# URL for authorising cookie\nHUB_API_URL=http://192.168.192.64:9000/jupyter\n# IP for listening request\nIP=0.0.0.0\n\n[ -f /home/jupyter/jupyter.config ] && . /home/jupyter/jupyter.config\n\n[ -z $IP ] && IP=$(ip address show dev eth0 | grep -P -o '10\\.[0-9]*\\.[0-9]*\\.[0-9]*(?=/)')\n\nDAEMON_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\"\n\n. /lib/lsb/init-functions\n\n###########\n\nstart-stop-daemon --start --oknodo --background -d $RUN_DIR --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS\n"
  },
  {
    "path": "tools/update-UserTable.sh",
    "content": "#!/bin/bash\n\necho \"Backup UserTable...\"\ncp /opt/docklet/global/sys/UserTable.db /opt/docklet/global/sys/UserTable.db.backup\n\nsed -i \"s/^    beans/#    beans/g\" ../src/model.py\nsed -i \"s/^        self.beans/#        self.beans/g\" ../src/model.py\n\necho \"Alter UserTable...\"\npython3 alterUserTable.py\n\ngit checkout -- ../\n\n"
  },
  {
    "path": "tools/update-basefs.sh",
    "content": "#!/bin/sh\n\n## WARNING\n## This sript is just for my own convenience . my image is\n## based on Ubuntu xenial. I did not test it for other distros.\n## Therefore this script may not work for your basefs image.\n##\n\n\nif [ \"$1\" != \"-y\" ] ; then \n    echo \"This script will update your basefs. backup it first.\"\n    echo \"then run:  $0 -y\"\n    exit 1\nfi \n\n\n# READ docklet.conf\n\nFS_PREFIX=/opt/docklet\n\nBASEFS=$FS_PREFIX/local/basefs\n\nCONF=../conf/docklet.conf\n\necho \"Reading $CONF\"\n\nif [ -f $CONF ] ; then\n    . $CONF\n    BASEFS=$FS_PREFIX/local/basefs\n    echo \"$CONF exit, basefs=$BASEFS\"\nelse\n    echo \"$CONF not exist, default basefs=$BASEFS\" \nfi\n\nif [ ! -d $BASEFS ] ; then\n    echo \"Checking $BASEFS: not exist, FAIL\"\n    exit 1\nelse\n    echo \"Checking $BASEFS: exist. \"\nfi\n\necho \"[*] Copying start_jupyter.sh to $BASEFS/home/jupyter\"\n\nmkdir -p $BASEFS/home/jupyter\n\ncp start_jupyter.sh $BASEFS/home/jupyter\n\necho \"\"\n\necho \"[*] Changing $BASEFS/etc/network/interfaces using static\"\n\necho \"Original network/interfaces is\"\n\ncat $BASEFS/etc/network/interfaces | sed 's/^/OLD    /'\n\nsed -i -- 's/dhcp/static/g' $BASEFS/etc/network/interfaces \n\n# setting resolv.conf, use your own resolv.conf for your image\necho \"[*] Setting $BASEFS/etc/resolv.conf\"\ncp resolv.conf $BASEFS/etc/resolvconf/resolv.conf.d/base\n\necho \"[*] Masking console-getty.service\"\nchroot $BASEFS systemctl mask console-getty.service\n\necho \"[*] Masking system-journald.service\"\nchroot $BASEFS systemctl mask systemd-journald.service\n\necho \"[*] Masking system-logind.service\"\nchroot $BASEFS systemctl mask systemd-logind.service\n\necho \"[*] Masking dbus.service\"\nchroot $BASEFS systemctl mask dbus.service\n\necho \"[*] Disabling apache2 service(if installed)\"\nif [ -d $BASEFS/etc/apache2 ] ; then\nchroot $BASEFS update-rc.d apache2 disable\nfi\n\necho \"[*] Disabling ondemand service(if installed)\"\nchroot $BASEFS update-rc.d ondemand disable\n\necho \"[*] Disabling dbus service(if installed)\"\nchroot $BASEFS update-rc.d dbus disable\n\necho \"[*] Disabling mysql service(if installed)\"\nif [ -d $BASEFS/etc/mysql ] ; then\nchroot $BASEFS update-rc.d mysql disable\nfi\n\necho \"[*] Disabling nginx service(if installed)\"\nif [ -d $BASEFS/etc/nginx ] ; then\nchroot $BASEFS update-rc.d nginx disable\nfi\n\necho \"[*] Setting worker_processes of nginx to 1(if installed)\"\n[ -f $BASEFS/etc/nginx/nginx.conf ] && sed -i -- 's/worker_processes\\ auto/worker_processes\\ 1/g' $BASEFS/etc/nginx/nginx.conf \n\necho \"[*] Deleting default /etc/nginx/sites-enabled/default\"\nrm -f  $BASEFS/etc/nginx/sites-enabled/default\n\necho \"[*] Copying vimrc.local to $BASEFS/etc/vim/\"\ncp vimrc.local $BASEFS/etc/vim\n\necho \"[*] Copying pip.conf to $BASEFS/root/.pip/\"\nmkdir -p $BASEFS/root/.pip/\ncp pip.conf $BASEFS/root/.pip\n\necho \"[*] Copying npmrc to $BASEFS/root/.npmrc\"\ncp npmrc $BASEFS/root/.npmrc\n\necho \"[*] Copying DOCKLET_NOTES.txt to $BASEFS/root/DOCKLET_NOTES.txt\"\ncp DOCKLET_NOTES.txt $BASEFS/root/\n\necho \"[*] Updating USER/.ssh/config to disable StrictHostKeyChecking\"\nfor f in $FS_PREFIX/global/users/* ; do \n    cat <<EOF > $f/ssh/config\nHost *\n    StrictHostKeyChecking no\n    UserKnownHostsFile=/dev/null\nEOF\ndone\n\necho \"[*] Generating $BASEFS/home/spark/sbin/dl_{start|stop}_spark.sh for Spark\"\nif [ -d $BASEFS/home/spark/sbin ] ; then\n    cp dl_*_spark.sh $BASEFS/home/spark/sbin\nfi\n\necho \"[*] Generating $BASEFS/root/{R|python}_demo.ipynb\"\nif [ -d $BASEFS/root/ ] ; then\n    cp R_demo.ipynb python_demo.ipynb $BASEFS/root/\nfi\n"
  },
  {
    "path": "tools/update_baseurl.sh",
    "content": "#!/bin/sh\n\ntoolsdir=${0%/*}\nDOCKLET_TOOLS=$(cd $toolsdir; pwd)\nDOCKLET_HOME=${DOCKLET_TOOLS%/*}\nDOCKLET_CONF=$DOCKLET_HOME/conf\n\n. $DOCKLET_CONF/docklet.conf\n\nmasterip=$(ifconfig ${NETWORK_DEVICE} | awk '/inet/ {print $2}' | awk -F: '{print $2}' | head -1)\ncons=$(ls /var/lib/lxc)\n\necho ${masterip}\nfor i in ${cons}\ndo\n    sed -i \"s/BASE_URL=\\/go/BASE_URL=\\/${masterip}\\/go/g\" /var/lib/lxc/${i}/rootfs/home/jupyter/jupyter.config\n    running=$(lxc-info -n ${i} | grep RUNNING)\n    if [ \"${running}\" != '' ]\n    then\n        echo \"Stop ${i}...\"\n        lxc-stop -k -n ${i}\n        echo \"Start ${i}...\"\n        lxc-start -n ${i}\n        lxc-attach -n ${i} -- su -c /home/jupyter/start_jupyter.sh\n        lxc-attach -n ${i} -- service ssh start\n    fi\ndone\n"
  },
  {
    "path": "tools/update_con_network.py",
    "content": "import sys,os\nsys.path.append(\"../src/\")\nimport env,requests\n\nif len(sys.argv) < 2:\n    print(\"Please enter USER_IP\")\n    exit()\n\nuserpoint = \"http://\" + sys.argv[1] + \":\" + str(env.getenv('USER_PORT'))\nauth_key = env.getenv('AUTH_KEY')\n\ndef post_to_user(url = '/', data={}):\n    return requests.post(userpoint+url,data=data).json()\n\ncons = os.listdir('/var/lib/lxc')\nfor con in cons:\n    print(\"Update %s...\"%(con))\n    namesplit = con.split('-')\n    user = namesplit[0]\n    res = post_to_user('/user/uid/',{'username':user,'auth_key':auth_key})\n    try:\n        configfile = open('/var/lib/lxc/'+con+'/config','r')\n    except:\n        continue\n    context = configfile.read()\n    configfile.close()\n    #print(context)\n    #print(res['uid'])\n    context = context.replace(\"docklet-br\",\"docklet-br-\"+str(res['uid']))\n    newfile = open('/var/lib/lxc/'+con+'/config','w')\n    newfile.write(context)\n    newfile.close()\n"
  },
  {
    "path": "tools/update_v0.3.2.py",
    "content": "import json\n\ndef isexist(quotas, key):\n    flag = False\n    for quota in quotas:\n        if quota['name'] == key:\n            flag = True\n            return flag\n    return flag\n\nfspath = '/opt/docklet'\ngroupfile = open(fspath+\"/global/sys/quota\",'r')\ngroups = json.loads(groupfile.read())\ngroupfile.close()\nfor group in groups:\n    group['quotas']['portmapping'] = 8\n    group['quotas']['input_rate_limit'] = 10000\n    group['quotas']['output_rate_limit'] = 10000\ngroupfile = open(fspath+\"/global/sys/quota\",'w')\ngroupfile.write(json.dumps(groups))\ngroupfile.close()\n\nquotafile = open(fspath+\"/global/sys/quotainfo\",'r')\nquotas = json.loads(quotafile.read())\nquotafile.close()\n\nif not isexist(quotas['quotainfo'], 'portmapping'):\n    quotas['quotainfo'].append({'name':'portmapping', 'hint':'how many ports the user can map, e.g. 8'})\nif not isexist(quotas['quotainfo'], 'input_rate_limit'):\n    quotas['quotainfo'].append({'name':'input_rate_limit', 'hint':'the ingress speed of the network, number of kbps. 0 means the rate are unlimited.'})\nif not isexist(quotas['quotainfo'], 'output_rate_limit'):\n    quotas['quotainfo'].append({'name':'output_rate_limit', 'hint':'the egress speed of the network, number of kbps. 0 means the rate are unlimited.'})\nquotafile = open(fspath+\"/global/sys/quotainfo\",'w')\nquotafile.write(json.dumps(quotas))\nquotafile.close()\n"
  },
  {
    "path": "tools/upgrade.py",
    "content": "#!/usr/bin/python3\n\nimport os, json, sys\nsys.path.append(\"../src/\")\nfrom model import db, User\nfrom lvmtool import sys_run\nfspath=\"/opt/docklet\"\n\ndef update_quotainfo():\n    if not os.path.exists(fspath+\"/global/sys/quotainfo\"):\n        print(\"quotainfo file not exists, please run docklet to init it\")\n        return False\n    quotafile = open(fspath+\"/global/sys/quotainfo\", 'r')\n    quotas = json.loads(quotafile.read())\n    quotafile.close()\n    if type(quotas) is list:\n        new_quotas = {}\n        new_quotas['default'] = 'foundation'\n        new_quotas['quotainfo'] = quotas\n        quotas = new_quotas\n        print(\"change the type of quotafile from list to dict\")\n    keys = []\n    for quota in quotas['quotainfo']:\n        keys.append(quota['name'])\n    if 'cpu' not in keys:\n        quotas['quotainfo'].append({'name':'cpu', 'hint':'the cpu quota, number of cores, e.g. 4'})\n    if 'memory' not in keys:\n        quotas['quotainfo'].append({'name':'memory', 'hint':'the memory quota, number of MB, e.g. 4000'})\n    if 'disk' not in keys:\n        quotas['quotainfo'].append({'name':'disk', 'hint':'the disk quota, number of MB, e.g. 4000'})\n    if 'data' not in keys:\n        quotas['quotainfo'].append({'name':'data', 'hint':'the quota of data space, number of GB, e.g. 100'})\n    if 'image' not in keys:\n        quotas['quotainfo'].append({'name':'image', 'hint':'how many images the user can have, e.g. 8'})\n    if 'idletime' not in keys:\n        quotas['quotainfo'].append({'name':'idletime', 'hint':'will stop cluster after idletime, number of hours, e.g. 24'})\n    if 'vnode' not in keys:\n        quotas['quotainfo'].append({'name':'vnode', 'hint':'how many containers the user can have, e.g. 8'})\n    print(\"quotainfo updated\")\n    quotafile = open(fspath+\"/global/sys/quotainfo\", 'w')\n    quotafile.write(json.dumps(quotas))\n    quotafile.close()\n    if not os.path.exists(fspath+\"/global/sys/quota\"):\n        print(\"quota file not exists, please run docklet to init it\")\n        return False\n    groupfile = open(fspath+\"/global/sys/quota\",'r')\n    groups = json.loads(groupfile.read())\n    groupfile.close()\n    for group in groups:\n        if 'cpu' not in group['quotas'].keys():\n            group['quotas']['cpu'] = \"4\"\n        if 'memory' not in group['quotas'].keys():\n            group['quotas']['memory'] = \"2000\"\n        if 'disk' not in group['quotas'].keys():\n            group['quotas']['disk'] = \"2000\"\n        if 'data' not in group['quotas'].keys():\n            group['quotas']['data'] = \"100\"\n        if 'image' not in group['quotas'].keys():\n            group['quotas']['image'] = \"10\"\n        if 'idletime' not in group['quotas'].keys():\n            group['quotas']['idletime'] = \"24\"\n        if 'vnode' not in group['quotas'].keys():\n            group['quotas']['vnode'] = \"8\"\n    print(\"quota updated\")\n    groupfile = open(fspath+\"/global/sys/quota\",'w')\n    groupfile.write(json.dumps(groups))\n    groupfile.close()\n\n\ndef name_error():\n    quotafile = open(fspath+\"/global/sys/quotainfo\", 'r')\n    quotas = json.loads(quotafile.read())\n    quotafile.close()\n    if quotas['default'] == 'fundation':\n        quotas['default'] = 'foundation'\n    quotafile = open(fspath+\"/global/sys/quotainfo\",'w')\n    quotafile.write(json.dumps(quotas)) \n    quotafile.close()\n\n    groupfile = open(fspath+\"/global/sys/quota\", 'r')\n    groups = json.loads(groupfile.read())\n    groupfile.close()\n    for group in groups:\n        if group['name'] == 'fundation':\n            group['name'] = 'foundation'\n    groupfile = open(fspath+\"/global/sys/quota\",'w')\n    groupfile.write(json.dumps(groups)) \n    groupfile.close()\n    \n    users = User.query.filter_by(user_group = 'fundation').all()\n    for user in users:\n        user.user_group = 'foundation'\n    db.session.commit()\n    \n\ndef allquota():\n    try:\n        quotafile = open(fspath+\"/global/sys/quota\", 'r')\n        quotas = json.loads(quotafile.read())\n        quotafile.close()\n        return quotas\n    except Exception as e:\n        print(e)\n        return None\n\ndef quotaquery(quotaname,quotas):\n    for quota in quotas:\n        if quota['name'] == quotaname:\n            return quota['quotas']\n    return None\n\ndef enable_gluster_quota():\n    conffile=open(\"../conf/docklet.conf\",'r')\n    conf=conffile.readlines()\n    conffile.close()\n    enable = False\n    volume_name = \"\"\n    for line in conf:\n        if line.startswith(\"DATA_QUOTA\"):\n            keyvalue = line.split(\"=\")\n            if len(keyvalue) < 2:\n                continue\n            key = keyvalue[0].strip()\n            value = keyvalue[1].strip()\n            if value == \"YES\":\n                enable = True\n                break\n    for line in conf:\n        if line.startswith(\"DATA_QUOTA_CMD\"):\n            keyvalue = line.split(\"=\")\n            if len(keyvalue) < 2:\n                continue\n            volume_name = keyvalue[1].strip()\n    if not enable:\n        print(\"don't need to enable the quota\")\n        return\n    \n    users = User.query.all()\n    quotas = allquota()\n    if quotaquery == None:\n        print(\"quota info not found\")\n        return\n    sys_run(\"gluster volume quota %s enable\" % volume_name)\n    for user in users:\n        quota = quotaquery(user.user_group, quotas)\n        nfs_quota = quota['data']\n        if nfs_quota == None:\n            print(\"data quota should be set\")\n            return\n        nfspath = \"/users/%s/data\" % user.username\n        sys_run(\"gluster volume quota %s limit-usage %s %sGB\" % (volume_name,nfspath,nfs_quota))\n\ndef update_image():\n    private_imagepath = fspath + \"/global/images/private/\"\n    public_imagepath = fspath + \"/global/images/public/\"\n    userdirs = os.listdir(private_imagepath)\n    for userdir in userdirs:\n        if os.path.isdir(private_imagepath+userdir+\"/\"):\n            currentdir = private_imagepath+userdir+\"/\"\n            images = os.listdir(currentdir)\n            for image in images:\n                if os.path.isdir(currentdir+image+\"/\"):\n                    try:\n                        sys_run(\"tar -cvf %s -C %s .\" % (currentdir+image+\".tz\",currentdir+image))\n                        #sys_run(\"rm -rf %s\" % currentdir+image)\n                    except Exception as e:\n                        print(e)\n    userdirs = os.listdir(public_imagepath)\n    for userdir in userdirs:\n        if os.path.isdir(public_imagepath+userdir+\"/\"):\n            currentdir = public_imagepath+userdir+\"/\"\n            images = os.listdir(currentdir)\n            for image in images:\n                if os.path.isdir(currentdir+image+\"/\"):\n                    try:\n                        sys_run(\"tar -cvf %s -C %s .\" % (currentdir+image+\".tz\",currentdir+image))\n                        #sys_run(\"rm -rf %s\" % currentdir+image)\n                    except Exception as e:\n                        print(e)\n\nif __name__ == '__main__':\n#    update_quotainfo()\n    if \"fix-name-error\" in sys.argv:\n        name_error()\n#    enable_gluster_quota()\n    if \"update-image\" in sys.argv:\n        update_image()\n"
  },
  {
    "path": "tools/upgrade_file2db.py",
    "content": "import sys\nsys.path.append(\"../src/\")\nimport os,json\nfrom datetime import datetime\nfrom model import db, VCluster, Container, PortMapping, Image, BillingHistory\n\ntimeFormat = \"%Y-%m-%d %H:%M:%S\"\ndockletPath = \"/opt/docklet/global\"\nusersdir = dockletPath + \"/users/\"\n\ntry:\n    VCluster.query.all()\nexcept Exception as err:\n    print(\"Create database...\")\n    db.create_all()\n\nprint(\"Update vcluster...\")\nfor user in os.listdir(usersdir):\n    tmppath = usersdir+user+\"/clusters/\"\n    if not os.path.exists(tmppath):\n        continue\n    print(\"Update User: \"+str(user))\n    clusterfiles = os.listdir(tmppath)\n    for cluname in clusterfiles:\n        cluFile = open(tmppath+cluname,\"r\")\n        cluinfo = json.loads(cluFile.read())\n        vcluster = VCluster(cluinfo['clusterid'],cluname,user,cluinfo['status'],cluinfo['size'],cluinfo['nextcid'],cluinfo['proxy_server_ip'],cluinfo['proxy_public_ip'])\n        vcluster.create_time = datetime.strptime(cluinfo['create_time'],timeFormat)\n        vcluster.start_time = cluinfo['start_time']\n        for coninfo in cluinfo['containers']:\n            lastsavet = datetime.strptime(coninfo['lastsave'],timeFormat)\n            con = Container(coninfo['containername'], coninfo['hostname'], coninfo['ip'], coninfo['host'], coninfo['image'], lastsavet, coninfo['setting'])\n            vcluster.containers.append(con)\n        for pminfo in cluinfo['port_mapping']:\n            pm = PortMapping(pminfo['node_name'], pminfo['node_ip'], int(pminfo['node_port']), int(pminfo['host_port']))\n            vcluster.port_mapping.append(pm)\n        if \"billing_history\" in cluinfo.keys():\n            for nodename in cluinfo['billing_history'].keys():\n                bhinfo = cluinfo['billing_history'][nodename]\n                bh = BillingHistory(nodename,bhinfo['cpu'],bhinfo['mem'],bhinfo['disk'],bhinfo['port'])\n                vcluster.billing_history.append(bh)\n        try:\n            db.session.add(vcluster)\n            db.session.commit()\n        except Exception as err:\n            print(err)\n        cluFile.close()\n\nprint(\"Update Images...\")\nfor shareStr in ['private/','public/']:\n    print(\"Update \"+shareStr+\" Images...\")\n    for user in os.listdir(dockletPath+\"/images/\"+shareStr):\n        print(\"Update User: \"+user)\n        tmppath = dockletPath+\"/images/\"+shareStr+user+\"/\"\n        files = os.listdir(tmppath)\n        images = []\n        for file in files:\n            if file[0] == \".\" or file[-3] != \".\":\n                continue\n            images.append(file[:-3])\n        for img in images:\n            infofile = open(tmppath+\".\"+img+\".info\",\"r\")\n            imginfo = infofile.read().split('\\n')\n            infofile.close()\n            desfile = open(tmppath+\".\"+img+\".description\",\"r\")\n            desinfo = desfile.read()\n            dbimage = Image.query.filter_by(imagename=img,ownername=user).first()\n            if dbimage is None:\n                dbimage = Image(img,False,False,user,desinfo)\n                dbimage.create_time = datetime.strptime(imginfo[0],timeFormat)\n            if shareStr == 'public/':\n                dbimage.hasPublic = True\n            else:\n                dbimage.hasPrivate = True\n            try:\n                db.session.add(dbimage)\n                db.session.commit()\n            except Exception as err:\n                print(err)\nprint(\"Finished!\")\n"
  },
  {
    "path": "tools/vimrc.local",
    "content": "syntax on\n\nset smarttab expandtab sw=4 ts=4\n\nset sm ai\n\nset hlsearch\n\nset wildchar=<Tab> wildmenu wildmode=full\n\nset enc=utf-8\nset fileencoding=utf-8\nset fileencodings=utf-8,cp936,euc-cn,ascii\n\nfiletype indent on\n"
  },
  {
    "path": "user/stopreqmgr.py",
    "content": "import threading, time\nfrom httplib2 import Http\nfrom urllib.parse import urlencode\nfrom queue import Queue\nfrom utils import tools, env\nfrom utils.log import logger\n\nmasterips = env.getenv(\"MASTER_IPS\").split(\",\")\nG_masterips = []\nfor masterip in masterips:\n    G_masterips.append(masterip.split(\"@\")[0] + \":\" + str(env.getenv(\"MASTER_PORT\")))\n\n# send http request to master\ndef request_master(url,data):\n    global G_masterips\n    #logger.info(\"master_ip:\"+str(G_masterip))\n    header = {'Content-Type':'application/x-www-form-urlencoded'}\n    http = Http()\n    for masterip in G_masterips:\n        [resp,content] = http.request(\"http://\"+masterip+url,\"POST\",urlencode(data),headers = header)\n        logger.info(\"response from master:\"+content.decode('utf-8'))\n\nclass StopAllReqMgr(threading.Thread):\n    def __init__(self, maxsize=100, interval=1):\n        threading.Thread.__init__(self)\n        self.thread_stop = False\n        self.interval = 1\n        self.q = Queue(maxsize=maxsize)\n\n    def add_request(self,username):\n        self.q.put(username)\n\n    def run(self):\n        while not self.thread_stop:\n            username = self.q.get()\n            logger.info(\"The beans of User(\" + str(username) + \") are less than or equal to zero, all his or her vclusters will be stopped.\")\n            auth_key = env.getenv('AUTH_KEY')\n            form = {'username':username, 'auth_key':auth_key}\n            request_master(\"/cluster/stopall/\",form)\n            self.q.task_done()\n\n            time.sleep(self.interval)\n\n    def stop(self):\n        self.thread_stop = True\n        return\n"
  },
  {
    "path": "user/user.py",
    "content": "#!/usr/bin/python3\nimport json\nimport os\nimport getopt\n\nimport sys, inspect\n\n\n\n\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))\nsrc_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,\"..\", \"src\")))\nif src_folder not in sys.path:\n    sys.path.insert(0, src_folder)\n\nfrom utils import tools, env\nconfig = env.getenv(\"CONFIG\")\ntools.loadenv(config)\n\n# must first init loadenv\nfrom utils.log import initlogging\ninitlogging(\"docklet-user\")\nfrom utils.log import logger\n\nfrom flask import Flask, request, session, render_template, redirect, send_from_directory, make_response, url_for, abort\nfrom functools import wraps\nfrom master import userManager,beansapplicationmgr, notificationmgr, lockmgr\nimport threading,traceback\nfrom utils.model import User,db\nfrom httplib2 import Http\nfrom urllib.parse import urlencode\nfrom master.settings import settings\nfrom master.bugreporter import send_bug_mail\nfrom stopreqmgr import StopAllReqMgr\n\nexternal_login = env.getenv('EXTERNAL_LOGIN')\nif(external_login == 'TRUE'):\n    from userDependence import external_auth\n\napp = Flask(__name__)\n\n\ndef login_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        global G_usermgr\n        logger.info (\"get request, path: %s\" % request.path)\n        token = request.form.get(\"token\", None)\n        if (token == None):\n            return json.dumps({'success':'false', 'message':'user or key is null'})\n        cur_user = G_usermgr.auth_token(token)\n        if (cur_user == None):\n            return json.dumps({'success':'false', 'message':'token failed or expired', 'Unauthorized': 'True'})\n        return func(cur_user, cur_user.username, request.form, *args, **kwargs)\n\n    return wrapper\n\ndef auth_key_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        key_1 = env.getenv('AUTH_KEY')\n        key_2 = request.form.get(\"auth_key\",None)\n        #logger.info(str(ip) + \" \" + str(G_userip))\n        if key_2 is not None and key_1 == key_2:\n           return func(*args, **kwargs)\n        else:\n           return json.dumps({'success':'false','message': 'auth_key is required!'})\n\n    return wrapper\n\n@app.route(\"/login/\", methods=['POST'])\ndef login():\n    global G_usermgr\n    logger.info(\"handle request : user login\")\n    user = request.form.get(\"user\", None)\n    key = request.form.get(\"key\", None)\n    userip = request.form.get(\"ip\", \"\")\n    if user == None or key == None:\n        return json.dumps({'success': 'false', 'message':'user or key is null'})\n    auth_result = G_usermgr.auth(user,key,userip)\n    if auth_result['success'] == 'false':\n        logger.info(\"%s login failed\" % user)\n        return json.dumps({'success':'false', 'message':auth_result['reason']})\n    logger.info(\"%s login success\" % user)\n    return json.dumps({'success':'true', 'action':'login', 'data': auth_result['data']})\n\n@app.route('/external_login/', methods=['POST'])\ndef external_login():\n    global G_usermgr\n    logger.info(\"handle request : external user login\")\n    userip = request.form.get(\"ip\", \"\")\n    try:\n        result = G_usermgr.auth_external(request.form,userip)\n        return json.dumps(result)\n    except:\n        result = {'success':'false', 'reason':'Something wrong happened when auth an external account'}\n        return json.dumps(result)\n\n@app.route(\"/register/\", methods=['POST'])\ndef register():\n    global G_usermgr\n    if request.form.get('activate', None) == None:\n        logger.info (\"handle request : user register\")\n        username = request.form.get('username', '')\n        password = request.form.get('password', '')\n        email = request.form.get('email', '')\n        description = request.form.get('description','')\n        if (username == '' or password == '' or email == ''):\n            return json.dumps({'success':'false'})\n        newuser = G_usermgr.newuser()\n        newuser.username = request.form.get('username')\n        newuser.password = request.form.get('password','')\n        newuser.e_mail = request.form.get('email','')\n        newuser.student_number = request.form.get('studentnumber','')\n        newuser.department = request.form.get('department','')\n        newuser.nickname = request.form.get('truename','')\n        newuser.truename = request.form.get('truename','')\n        newuser.description = request.form.get('description','')\n        newuser.status = \"init\"\n        newuser.auth_method = \"local\"\n        result = G_usermgr.register(user = newuser)\n        return json.dumps(result)\n    else:\n        logger.info (\"handle request, user activating\")\n        token = request.form.get(\"token\", None)\n        if (token == None):\n            return json.dumps({'success':'false', 'message':'user or key is null'})\n        cur_user = G_usermgr.auth_token(token)\n        if (cur_user == None):\n            return json.dumps({'success':'false', 'message':'token failed or expired', 'Unauthorized': 'True'})\n        newuser = G_usermgr.newuser()\n        newuser.username = cur_user.username\n        newuser.nickname = cur_user.truename\n        newuser.password = cur_user.password\n        newuser.status = 'applying'\n        newuser.user_group = cur_user.user_group\n        newuser.auth_method = cur_user.auth_method\n        newuser.e_mail = request.form.get('email','')\n        newuser.student_number = request.form.get('studentnumber', '')\n        newuser.department = request.form.get('department', '')\n        newuser.truename = request.form.get('truename', '')\n        newuser.tel = request.form.get('tel', '')\n        newuser.description = request.form.get('description', '')\n        result = G_usermgr.register(user = newuser)\n        userManager.send_remind_activating_email(newuser.username)\n        return json.dumps(result)\n\n@app.route(\"/authtoken/\", methods=['POST'])\n@login_required\ndef auth_token(cur_user, user, form):\n     logger.info(\"authing\")\n     req = json.dumps({'success':'true','username':cur_user.username,'beans':cur_user.beans})\n     logger.info(\"auth success\")\n     return req\n\n\n@app.route(\"/user/modify/\", methods=['POST'])\n@login_required\ndef modify_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/modify/\")\n    result = G_usermgr.modify(newValue = form, cur_user = cur_user)\n    return json.dumps(result)\n\n@app.route(\"/user/groupModify/\", methods=['POST'])\n@login_required\ndef groupModify_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupModify/\")\n    G_lockmgr.acquire('__quotafile')\n    result = G_usermgr.groupModify(newValue = form, cur_user = cur_user)\n    G_lockmgr.release('__quotafile')\n    return json.dumps(result)\n\n\n@app.route(\"/user/query/\", methods=['POST'])\n@login_required\ndef query_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/query/\")\n    #result = G_usermgr.query(ID = form.get(\"ID\"), cur_user = cur_user)\n    if (form.get(\"ID\", None) != None):\n        result = G_usermgr.query(ID = form.get(\"ID\"), cur_user = cur_user)\n    else:\n        result = G_usermgr.query(username = user, cur_user = cur_user)\n    if (result.get('success', None) == None or result.get('success', None) == \"false\"):\n        return json.dumps(result)\n    else:\n        result = G_usermgr.queryForDisplay(user = result['token'])\n        return json.dumps(result)\n\n\n@app.route(\"/user/add/\", methods=['POST'])\n@login_required\ndef add_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/add/\")\n    user = G_usermgr.newuser(cur_user = cur_user)\n    user.username = form.get('username', None)\n    user.password = form.get('password', None)\n    user.e_mail = form.get('e_mail', '')\n    user.status = \"normal\"\n    result = G_usermgr.register(user = user, cur_user = cur_user)\n    return json.dumps(result)\n\n\n@app.route(\"/user/groupadd/\", methods=['POST'])\n@login_required\ndef groupadd_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupadd/\")\n    G_lockmgr.acquire('__quotafile')\n    result = G_usermgr.groupadd(form = form, cur_user = cur_user)\n    G_lockmgr.release('__quotafile')\n    return json.dumps(result)\n\n\n@app.route(\"/user/chdefault/\", methods=['POST'])\n@login_required\ndef chdefault(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/chdefault/\")\n    G_lockmgr.acquire('__quotafile')\n    result = G_usermgr.change_default_group(form = form, cur_user = cur_user)\n    G_lockmgr.release('__quotafile')\n    return json.dumps(result)\n\n\n@app.route(\"/user/quotaadd/\", methods=['POST'])\n@login_required\ndef quotaadd_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/quotaadd/\")\n    G_lockmgr.acquire('__quotafile')\n    result = G_usermgr.quotaadd(form = form, cur_user = cur_user)\n    G_lockmgr.release('__quotafile')\n    return json.dumps(result)\n\n\n@app.route(\"/user/groupdel/\", methods=['POST'])\n@login_required\ndef groupdel_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupdel/\")\n    G_lockmgr.acquire('__quotafile')\n    result = G_usermgr.groupdel(name = form.get('name', None), cur_user = cur_user)\n    G_lockmgr.release('__quotafile')\n    return json.dumps(result)\n\n\n@app.route(\"/user/data/\", methods=['POST'])\n@login_required\ndef data_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/data/\")\n    result = G_usermgr.userList(cur_user = cur_user)\n    return json.dumps(result)\n\n\n@app.route(\"/user/groupNameList/\", methods=['POST'])\n@login_required\ndef groupNameList_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupNameList/\")\n    result = G_usermgr.groupListName(cur_user = cur_user)\n    return json.dumps(result)\n\n\n@app.route(\"/user/groupList/\", methods=['POST'])\n@login_required\ndef groupList_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupList/\")\n    result = G_usermgr.groupList(cur_user = cur_user)\n    return json.dumps(result)\n\n\n@app.route(\"/user/groupQuery/\", methods=['POST'])\n@login_required\ndef groupQuery_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/groupQuery/\")\n    result = G_usermgr.groupQuery(name = form.get(\"name\"), cur_user = cur_user)\n    return json.dumps(result)\n\n\n@app.route(\"/user/selfQuery/\", methods=['POST'])\n@login_required\ndef selfQuery_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/selfQuery/\")\n    result = G_usermgr.selfQuery(cur_user = cur_user)\n    return json.dumps(result)\n\n@app.route(\"/master/user/recoverinfo/\", methods=['POST'])\n@auth_key_required\ndef get_master_recoverinfo():\n    username = request.form.get(\"username\",None)\n    if username is None:\n        return json.dumps({'success':'false', 'message':'username field is required.'})\n    else:\n        user = User.query.filter_by(username=username).first()\n        return json.dumps({'success':'true', 'uid':user.id, 'email':user.e_mail, 'groupname':user.user_group})\n\n@app.route(\"/master/user/groupinfo/\", methods=['POST'])\n@auth_key_required\ndef get_master_groupinfo():\n    fspath = env.getenv('FS_PREFIX')\n    groupfile = open(fspath+\"/global/sys/quota\",'r')\n    groups = json.loads(groupfile.read())\n    groupfile.close()\n    return json.dumps({'success':'true', 'groups':json.dumps(groups)})\n\n@app.route(\"/master/user/usageRelease/\", methods=['POST'])\n@auth_key_required\ndef usageRelease_master():\n    global G_usermgr\n    logger.info(\"handle request: /master/user/usageRelease/\")\n    form = request.form\n    user = form.get(\"username\",None)\n    cur_user = User.query.filter_by(username=user).first()\n    if user is None or cur_user is None:\n        return json.dumps({'success':'false', 'message':'Null username field or user does not exist.'})\n    G_lockmgr.acquire('__usage_'+str(user))\n    result = G_usermgr.usageRelease(cur_user = cur_user, cpu = form.get('cpu'), memory = form.get('memory'), disk = form.get('disk'))\n    G_lockmgr.release('__usage_'+str(user))\n    return json.dumps(result)\n\n@app.route(\"/user/selfModify/\", methods=['POST'])\n@login_required\ndef selfModify_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/selfModify/\")\n    result = G_usermgr.selfModify(cur_user = cur_user, newValue = form)\n    return json.dumps(result)\n\n@app.route(\"/user/usageQuery/\" , methods=['POST'])\n@login_required\ndef usageQuery_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/usageQuery/\")\n    result = G_usermgr.usageQuery(cur_user = cur_user)\n    return json.dumps(result)\n\n@app.route(\"/user/usageInc/\", methods=['POST'])\n@login_required\ndef usageInc_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/usageInc/\")\n    setting = form.get('setting')\n    G_lockmgr.acquire('__usage_'+str(user))\n    result = G_usermgr.usageInc(cur_user = cur_user, modification = json.loads(setting))\n    G_lockmgr.release('__usage_'+str(user))\n    return json.dumps(result)\n\n@app.route(\"/user/usageRelease/\", methods=['POST'])\n@login_required\ndef usageRelease_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/usageInc/\")\n    G_lockmgr.acquire('__usage_'+str(user))\n    result = G_usermgr.usageRelease(cur_user = cur_user, cpu = form.get('cpu'), memory = form.get('memory'), disk = form.get('disk'))\n    G_lockmgr.release('__usage_'+str(user))\n    return json.dumps(result)\n\n@app.route(\"/user/usageRecover/\", methods=['POST'])\n@login_required\ndef usageRecover_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/usageInc/\")\n    G_lockmgr.acquire('__usage_'+str(user))\n    result = G_usermgr.usageRecover(cur_user = cur_user, modification = json.loads(form.get('setting')))\n    G_lockmgr.release('__usage_'+str(user))\n    return json.dumps(result)\n\n@app.route(\"/user/lxcsettingList/\", methods=['POST'])\n@login_required\ndef lxcsettingList_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/lxcsettingList/\")\n    result = G_usermgr.lxcsettingList(cur_user = cur_user, form = form)\n    return json.dumps(result)\n\n@app.route(\"/user/chlxcsetting/\", methods=['POST'])\n@login_required\ndef chlxcsetting_user(cur_user, user, form):\n    global G_usermgr\n    logger.info(\"handle request: user/chlxcsetting/\")\n    G_lockmgr.acquire('__lxcsetting')\n    result = G_usermgr.chlxcsetting(cur_user = cur_user, form = form)\n    G_lockmgr.release('__lxcsetting')\n    return json.dumps(result)\n\n@app.route(\"/settings/list/\", methods=['POST'])\n@login_required\ndef settings_list(cur_user, user, form):\n    return json.dumps(settings.list(user_group = 'admin'))\n\n@app.route(\"/settings/update/\", methods=['POST'])\n@login_required\ndef settings_update(user, beans, form):\n    newSetting = {}\n    newSetting['OPEN_REGISTRY'] = form.get('OPEN_REGISTRY','')\n    newSetting['APPROVAL_RBT'] = form.get('APPROVAL_RBT','')\n    newSetting['ADMIN_EMAIL_ADDRESS'] = form.get('ADMIN_EMAIL_ADDRESS', '')\n    newSetting['EMAIL_FROM_ADDRESS'] = form.get('EMAIL_FROM_ADDRESS', '')\n    return json.dumps(settings.update(user_group = 'admin', newSetting = newSetting))\n\n@app.route(\"/notification/list/\", methods=['POST'])\n@login_required\ndef list_notifications(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/list/\")\n    result = G_notificationmgr.list_notifications(cur_user=cur_user, form=form)\n    return json.dumps(result)\n\n\n@app.route(\"/notification/create/\", methods=['POST'])\n@login_required\ndef create_notification(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/create/\")\n    G_lockmgr.acquire('__notification')\n    result = G_notificationmgr.create_notification(cur_user=cur_user, form=form)\n    G_lockmgr.release('__notification')\n    return json.dumps(result)\n\n\n@app.route(\"/notification/modify/\", methods=['POST'])\n@login_required\ndef modify_notification(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/modify/\")\n    G_lockmgr.acquire('__notification')\n    result = G_notificationmgr.modify_notification(cur_user=cur_user, form=form)\n    G_lockmgr.release('__notification')\n    return json.dumps(result)\n\n\n@app.route(\"/notification/delete/\", methods=['POST'])\n@login_required\ndef delete_notification(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/delete/\")\n    G_lockmgr.acquire('__notification')\n    result = G_notificationmgr.delete_notification(cur_user=cur_user, form=form)\n    G_lockmgr.release('__notification')\n    return json.dumps(result)\n\n\n@app.route(\"/notification/query_self/\", methods=['POST'])\n@login_required\ndef query_self_notification_simple_infos(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/query_self/\")\n    result = G_notificationmgr.query_self_notification_simple_infos(cur_user=cur_user, form=form)\n    return json.dumps(result)\n\n\n@app.route(\"/notification/query/\", methods=['POST'])\n@login_required\ndef query_notification(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/query/\")\n    result = G_notificationmgr.query_notification(cur_user=cur_user, form=form)\n    return json.dumps(result)\n\n\n@app.route(\"/notification/query/all/\", methods=['POST'])\n@login_required\ndef query_self_notifications_infos(cur_user, user, form):\n    global G_notificationmgr\n    logger.info(\"handle request: notification/query/all/\")\n    result = G_notificationmgr.query_self_notifications_infos(cur_user=cur_user, form=form)\n    return json.dumps(result)\n\n@app.route(\"/bug/report/\", methods=['POST'])\n@login_required\ndef report_bug(cur_user, user, form):\n    logger.info(\"handle request: bug/report\")\n    result = send_bug_mail(user, form.get(\"bugmessage\", None))\n    return json.dumps(result)\n\n@app.route(\"/billing/beans/\", methods=['POST'])\n@auth_key_required\ndef billing_beans():\n        logger.info(\"handle request: /billing/beans/\")\n        form = request.form\n        owner_name = form.get(\"owner_name\",None)\n        billing = int(form.get(\"billing\",None))\n        if owner_name is None  or billing is None:\n            return json.dumps({'success':'false', 'message':'owner_name and beans fields are required.'})\n        G_lockmgr.acquire('__beans_'+str(owner_name))\n        # update users' tables in database\n        owner = User.query.filter_by(username=owner_name).first()\n        if owner is None:\n            logger.warning(\"Error!!! Billing User %s doesn't exist!\" % (owner_name))\n        else:\n            #logger.info(\"Billing User:\"+str(owner))\n            oldbeans = owner.beans\n            owner.beans -= billing\n            #logger.info(str(oldbeans) + \" \" + str(owner.beans))\n            if oldbeans > 0 and owner.beans <= 0 or oldbeans >= 100 and owner.beans < 100 or oldbeans >= 500 and owner.beans < 500 or oldbeans >= 1000 and owner.beans < 1000:\n                # send mail to remind users of their beans if their beans decrease to 0,100,500 and 1000\n                data = {\"to_address\":owner.e_mail,\"username\":owner.username,\"beans\":owner.beans}\n                # request_master(\"/beans/mail/\",data)\n                beansapplicationmgr.send_beans_email(owner.e_mail,owner.username,int(owner.beans))\n            try:\n                db.session.commit()\n            except Exception as err:\n                db.session.rollback()\n                logger.warning(traceback.format_exc())\n                logger.warning(err)\n                G_lockmgr.release('__beans_'+str(owner_name))\n                return json.dumps({'success':'false', 'message':'Fail to wirte to databases.'})\n            #logger.info(\"Billing User:\"+str(owner))\n            if owner.beans <= 0:\n                # stop all vcluster of the user if his beans are equal to or lower than 0.\n                logger.info(\"The beans of User(\" + str(owner) + \") are less than or equal to zero, add request to queue.\")\n                G_stopreqmgr.add_request(owner.username)\n        G_lockmgr.release('__beans_'+str(owner_name))\n        return json.dumps({'success':'true'})\n\n@app.route(\"/beans/<issue>/\", methods=['POST'])\n@login_required\ndef beans_apply(cur_user,user,form,issue):\n    global G_applicationmgr\n    if issue == 'apply':\n        if not cur_user.status == 'normal':\n            return json.dumps({'success':'false', 'message':'Fail to apply for beans because your account is locked/not activated. Please:'+\n            '\\n 1. Complete your information and activate your account. \\n Or: \\n 2.Contact administor for further information'})\n        number = form.get(\"number\",None)\n        reason = form.get(\"reason\",None)\n        if number is None or reason is None:\n            return json.dumps({'success':'false', 'message':'Number and reason can\\'t be null.'})\n        G_lockmgr.acquire('__beansapply_'+str(user))\n        [success,message] = G_applicationmgr.apply(user,number,reason)\n        G_lockmgr.release('__beansapply_'+str(user))\n        if not success:\n            return json.dumps({'success':'false', 'message':message})\n        else:\n            return json.dumps({'success':'true'})\n    elif issue == 'applymsgs':\n        applymsgs = G_applicationmgr.query(user)\n        return json.dumps({'success':'true','applymsgs':applymsgs})\n    else:\n        return json.dumps({'success':'false','message':'Unsupported URL!'})\n\n@app.route(\"/beans/admin/<issue>/\", methods=['POST'])\n@login_required\ndef beans_admin(cur_user,user,form,issue):\n    global G_applicationmgr\n    if issue == 'applymsgs':\n        result = G_applicationmgr.queryUnRead(cur_user = cur_user)\n        logger.debug(\"applymsg success\")\n        return json.dumps(result)\n    elif issue == 'agree':\n        msgid = form.get(\"msgid\",None)\n        username = form.get(\"username\",None)\n        if msgid is None or username is None:\n            return json.dumps({'success':'false', 'message':'msgid and username can\\'t be null.'})\n        G_lockmgr.acquire(\"__beans_\"+str(username))\n        G_lockmgr.acquire(\"__applymsg_\"+str(msgid))\n        result = G_applicationmgr.agree(msgid, cur_user = cur_user)\n        G_lockmgr.release(\"__applymsg_\"+str(msgid))\n        G_lockmgr.release(\"__beans_\"+str(username))\n        return json.dumps(result)\n    elif issue == 'reject':\n        msgid = form.get(\"msgid\",None)\n        if msgid is None:\n            return json.dumps({'success':'false', 'message':'msgid can\\'t be null.'})\n        G_lockmgr.acquire(\"__applymsg_\"+str(msgid))\n        result = G_applicationmgr.reject(msgid, cur_user = cur_user)\n        G_lockmgr.release(\"__applymsg_\"+str(msgid))\n        return json.dumps(result)\n    else:\n        return json.dumps({'success':'false', 'message':'Unsupported URL!'})\n\n@app.errorhandler(500)\ndef internal_server_error(error):\n    logger.debug(\"An internel server error occured\")\n    logger.error(traceback.format_exc())\n    return json.dumps({'success':'false', 'message':'500 Internal Server Error', 'Unauthorized': 'True'})\n\n\nif __name__  ==  '__main__':\n    logger.info('Start Flask...:')\n    try:\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/user_secret_key.txt')\n        app.secret_key = secret_key_file.read()\n        secret_key_file.close()\n    except:\n        from base64 import b64encode\n        from os import urandom\n        secret_key = urandom(24)\n        secret_key = b64encode(secret_key).decode('utf-8')\n        app.secret_key = secret_key\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/user_secret_key.txt', 'w')\n        secret_key_file.write(secret_key)\n        secret_key_file.close()\n\n    os.environ['APP_KEY'] = app.secret_key\n    runcmd = sys.argv[0]\n    app.runpath = runcmd.rsplit('/', 1)[0]\n\n    global G_usermgr\n    global G_notificationmgr\n    global G_sysmgr\n    global G_historymgr\n    global G_applicationmgr\n    global G_lockmgr\n    global G_stopreqmgr\n\n    fs_path = env.getenv(\"FS_PREFIX\")\n    logger.info(\"using FS_PREFIX %s\" % fs_path)\n\n    mode = 'recovery'\n    if len(sys.argv) > 1 and sys.argv[1] == \"new\":\n        mode = 'new'\n\n    G_lockmgr = lockmgr.LockMgr()\n    G_usermgr = userManager.userManager('root')\n    #if mode == \"new\":\n    #    G_usermgr.initUsage()\n    G_notificationmgr = notificationmgr.NotificationMgr()\n\n    #userip = env.getenv('USER_IP')\n    userip = \"0.0.0.0\"\n    logger.info(\"using USER_IP %s\", userip)\n\n    #userport = env.getenv('USER_PORT')\n    userport = 9100\n    logger.info(\"using USER_PORT %d\", int(userport))\n\n    G_applicationmgr = beansapplicationmgr.ApplicationMgr()\n    approvalrbt = beansapplicationmgr.ApprovalRobot()\n    if(env.getenv(\"APPROVAL_RBT\") == \"ON\"):\n        approvalrbt .start()\n        logger.info(\"ApprovalRobot is started.\")\n    else:\n        logger.info(\"ApprovalRobot is not started.\")\n\n    G_stopreqmgr = StopAllReqMgr()\n    G_stopreqmgr.start()\n\n    # server = http.server.HTTPServer((masterip, masterport), DockletHttpHandler)\n    logger.info(\"starting user server\")\n\n    app.run(host = userip, port = userport, threaded=True,)\n"
  },
  {
    "path": "web/static/css/docklet.css",
    "content": ".btn-outline, .btn-outline-default, .badge-outline, .badge-outline-default, .label-outline, .label-outline-default {\n\tborder: 1px solid #AAB2BD;\n\tbackground-color: transparent;\n\tcolor: #434A54;\n}\n.btn-outline-success, .badge-outline-success, .label-outline-success {\n\tborder: 1px solid #1C84C6;\n\tbackground-color: transparent;\n\tcolor: #1C84C6;\n}\n.btn-outline-warning, .badge-outline-warning, .label-outline-warning {\n\tborder: 1px solid #F8AC59;\n\tbackground-color: transparent;\n\tcolor: #F8AC59;\n}\n\n.btn-outline-default:hover,\n.btn-outline:hover {\n\tborder: 1px solid #AAB2BD;\n\tbackground-color: #AAB2BD;\n\tcolor: #434A54;\n}\n\n.btn-outline-success:hover {\n\tborder: 1px solid #1C84C6;\n\tbackground-color: #1C84C6;\n\tcolor: #FFFFFF;\n}\n\n.btn-outline-warning:hover {\n\tborder: 1px solid #F8AC59;\n\tbackground-color: #F8AC59;\n\tcolor: #FFFFFF;\n}\n.docklet-red-block{\n\tbackground-color: #EB4235;\n\tcolor: #FFFFFF;\n}\n\n.docklet-green-block{\n\tbackground-color: #7DB600;\n\tcolor: #FFFFFF;\n}\n\n.docklet-yellow-block{\n\tbackground-color: #FABC05;\n\tcolor: #FFFFFF;\n}\n\n.docklet-blue-block{\n\tbackground-color: #4185F6;\n\tcolor: #FFFFFF;\n}\n\n"
  },
  {
    "path": "web/static/dist/css/AdminLTE.css",
    "content": "/*!\n *   AdminLTE v2.3.2\n *   Author: Almsaeed Studio\n *\t Website: Almsaeed Studio <http://almsaeedstudio.com>\n *   License: Open source - MIT\n *           Please visit http://opensource.org/licenses/MIT for more information\n!*/\n/*\n * Core: General Layout Style\n * -------------------------\n */\nhtml,\nbody {\n  min-height: 100%;\n}\n.layout-boxed html,\n.layout-boxed body {\n  height: 100%;\n}\nbody {\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  font-family: 'Source Sans Pro', 'Helvetica Neue', Helvetica, Arial, sans-serif;\n  font-weight: 400;\n  overflow-x: hidden;\n  overflow-y: auto;\n}\n/* Layout */\n.wrapper {\n  min-height: 100%;\n  position: relative;\n  overflow: hidden;\n}\n.wrapper:before,\n.wrapper:after {\n  content: \" \";\n  display: table;\n}\n.wrapper:after {\n  clear: both;\n}\n.layout-boxed .wrapper {\n  max-width: 1250px;\n  margin: 0 auto;\n  min-height: 100%;\n  box-shadow: 0 0 8px rgba(0, 0, 0, 0.5);\n  position: relative;\n}\n.layout-boxed {\n  background: url('../img/boxed-bg.jpg') repeat fixed;\n}\n/*\n * Content Wrapper - contains the main content\n * ```.right-side has been deprecated as of v2.0.0 in favor of .content-wrapper  ```\n */\n.content-wrapper,\n.right-side,\n.main-footer {\n  -webkit-transition: -webkit-transform 0.3s ease-in-out, margin 0.3s ease-in-out;\n  -moz-transition: -moz-transform 0.3s ease-in-out, margin 0.3s ease-in-out;\n  -o-transition: -o-transform 0.3s ease-in-out, margin 0.3s ease-in-out;\n  transition: transform 0.3s ease-in-out, margin 0.3s ease-in-out;\n  margin-left: 230px;\n  z-index: 820;\n}\n.layout-top-nav .content-wrapper,\n.layout-top-nav .right-side,\n.layout-top-nav .main-footer {\n  margin-left: 0;\n}\n@media (max-width: 767px) {\n  .content-wrapper,\n  .right-side,\n  .main-footer {\n    margin-left: 0;\n  }\n}\n@media (min-width: 768px) {\n  .sidebar-collapse .content-wrapper,\n  .sidebar-collapse .right-side,\n  .sidebar-collapse .main-footer {\n    margin-left: 0;\n  }\n}\n@media (max-width: 767px) {\n  .sidebar-open .content-wrapper,\n  .sidebar-open .right-side,\n  .sidebar-open .main-footer {\n    -webkit-transform: translate(230px, 0);\n    -ms-transform: translate(230px, 0);\n    -o-transform: translate(230px, 0);\n    transform: translate(230px, 0);\n  }\n}\n.content-wrapper,\n.right-side {\n  min-height: 100%;\n  background-color: #ecf0f5;\n  z-index: 800;\n}\n.main-footer {\n  background: #fff;\n  padding: 15px;\n  color: #444;\n  border-top: 1px solid #d2d6de;\n}\n/* Fixed layout */\n.fixed .main-header,\n.fixed .main-sidebar,\n.fixed .left-side {\n  position: fixed;\n}\n.fixed .main-header {\n  top: 0;\n  right: 0;\n  left: 0;\n}\n.fixed .content-wrapper,\n.fixed .right-side {\n  padding-top: 50px;\n}\n@media (max-width: 767px) {\n  .fixed .content-wrapper,\n  .fixed .right-side {\n    padding-top: 100px;\n  }\n}\n.fixed.layout-boxed .wrapper {\n  max-width: 100%;\n}\nbody.hold-transition .content-wrapper,\nbody.hold-transition .right-side,\nbody.hold-transition .main-footer,\nbody.hold-transition .main-sidebar,\nbody.hold-transition .left-side,\nbody.hold-transition .main-header > .navbar,\nbody.hold-transition .main-header .logo {\n  /* Fix for IE */\n  -webkit-transition: none;\n  -o-transition: none;\n  transition: none;\n}\n/* Content */\n.content {\n  min-height: 250px;\n  padding: 15px;\n  margin-right: auto;\n  margin-left: auto;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n/* H1 - H6 font */\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n  font-family: 'Source Sans Pro', sans-serif;\n}\n/* General Links */\na {\n  color: #3c8dbc;\n}\na:hover,\na:active,\na:focus {\n  outline: none;\n  text-decoration: none;\n  color: #72afd2;\n}\n/* Page Header */\n.page-header {\n  margin: 10px 0 20px 0;\n  font-size: 22px;\n}\n.page-header > small {\n  color: #666;\n  display: block;\n  margin-top: 5px;\n}\n/*\n * Component: Main Header\n * ----------------------\n */\n.main-header {\n  position: relative;\n  max-height: 100px;\n  z-index: 1030;\n}\n.main-header > .navbar {\n  -webkit-transition: margin-left 0.3s ease-in-out;\n  -o-transition: margin-left 0.3s ease-in-out;\n  transition: margin-left 0.3s ease-in-out;\n  margin-bottom: 0;\n  margin-left: 230px;\n  border: none;\n  min-height: 50px;\n  border-radius: 0;\n}\n.layout-top-nav .main-header > .navbar {\n  margin-left: 0;\n}\n.main-header #navbar-search-input.form-control {\n  background: rgba(255, 255, 255, 0.2);\n  border-color: transparent;\n}\n.main-header #navbar-search-input.form-control:focus,\n.main-header #navbar-search-input.form-control:active {\n  border-color: rgba(0, 0, 0, 0.1);\n  background: rgba(255, 255, 255, 0.9);\n}\n.main-header #navbar-search-input.form-control::-moz-placeholder {\n  color: #ccc;\n  opacity: 1;\n}\n.main-header #navbar-search-input.form-control:-ms-input-placeholder {\n  color: #ccc;\n}\n.main-header #navbar-search-input.form-control::-webkit-input-placeholder {\n  color: #ccc;\n}\n.main-header .navbar-custom-menu,\n.main-header .navbar-right {\n  float: right;\n}\n@media (max-width: 991px) {\n  .main-header .navbar-custom-menu a,\n  .main-header .navbar-right a {\n    color: inherit;\n    background: transparent;\n  }\n}\n@media (max-width: 767px) {\n  .main-header .navbar-right {\n    float: none;\n  }\n  .navbar-collapse .main-header .navbar-right {\n    margin: 7.5px -15px;\n  }\n  .main-header .navbar-right > li {\n    color: inherit;\n    border: 0;\n  }\n}\n.main-header .sidebar-toggle {\n  float: left;\n  background-color: transparent;\n  background-image: none;\n  padding: 15px 15px;\n  font-family: fontAwesome;\n}\n.main-header .sidebar-toggle:before {\n  content: \"\\f0c9\";\n}\n.main-header .sidebar-toggle:hover {\n  color: #fff;\n}\n.main-header .sidebar-toggle:focus,\n.main-header .sidebar-toggle:active {\n  background: transparent;\n}\n.main-header .sidebar-toggle .icon-bar {\n  display: none;\n}\n.main-header .navbar .nav > li.user > a > .fa,\n.main-header .navbar .nav > li.user > a > .glyphicon,\n.main-header .navbar .nav > li.user > a > .ion {\n  margin-right: 5px;\n}\n.main-header .navbar .nav > li > a > .label {\n  position: absolute;\n  top: 9px;\n  right: 7px;\n  text-align: center;\n  font-size: 9px;\n  padding: 2px 3px;\n  line-height: .9;\n}\n.main-header .logo {\n  -webkit-transition: width 0.3s ease-in-out;\n  -o-transition: width 0.3s ease-in-out;\n  transition: width 0.3s ease-in-out;\n  display: block;\n  float: left;\n  height: 50px;\n  font-size: 20px;\n  line-height: 50px;\n  text-align: center;\n  width: 230px;\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  padding: 0 15px;\n  font-weight: 300;\n  overflow: hidden;\n}\n.main-header .logo .logo-lg {\n  display: block;\n}\n.main-header .logo .logo-mini {\n  display: none;\n}\n.main-header .navbar-brand {\n  color: #fff;\n}\n.content-header {\n  position: relative;\n  padding: 15px 15px 0 15px;\n}\n.content-header > h1 {\n  margin: 0;\n  font-size: 24px;\n}\n.content-header > h1 > small {\n  font-size: 15px;\n  display: inline-block;\n  padding-left: 4px;\n  font-weight: 300;\n}\n.content-header > .breadcrumb {\n  float: right;\n  background: transparent;\n  margin-top: 0;\n  margin-bottom: 0;\n  font-size: 12px;\n  padding: 7px 5px;\n  position: absolute;\n  top: 15px;\n  right: 10px;\n  border-radius: 2px;\n}\n.content-header > .breadcrumb > li > a {\n  color: #444;\n  text-decoration: none;\n  display: inline-block;\n}\n.content-header > .breadcrumb > li > a > .fa,\n.content-header > .breadcrumb > li > a > .glyphicon,\n.content-header > .breadcrumb > li > a > .ion {\n  margin-right: 5px;\n}\n.content-header > .breadcrumb > li + li:before {\n  content: '>\\00a0';\n}\n@media (max-width: 991px) {\n  .content-header > .breadcrumb {\n    position: relative;\n    margin-top: 5px;\n    top: 0;\n    right: 0;\n    float: none;\n    background: #d2d6de;\n    padding-left: 10px;\n  }\n  .content-header > .breadcrumb li:before {\n    color: #97a0b3;\n  }\n}\n.navbar-toggle {\n  color: #fff;\n  border: 0;\n  margin: 0;\n  padding: 15px 15px;\n}\n@media (max-width: 991px) {\n  .navbar-custom-menu .navbar-nav > li {\n    float: left;\n  }\n  .navbar-custom-menu .navbar-nav {\n    margin: 0;\n    float: left;\n  }\n  .navbar-custom-menu .navbar-nav > li > a {\n    padding-top: 15px;\n    padding-bottom: 15px;\n    line-height: 20px;\n  }\n}\n@media (max-width: 767px) {\n  .main-header {\n    position: relative;\n  }\n  .main-header .logo,\n  .main-header .navbar {\n    width: 100%;\n    float: none;\n  }\n  .main-header .navbar {\n    margin: 0;\n  }\n  .main-header .navbar-custom-menu {\n    float: right;\n  }\n}\n@media (max-width: 991px) {\n  .navbar-collapse.pull-left {\n    float: none !important;\n  }\n  .navbar-collapse.pull-left + .navbar-custom-menu {\n    display: block;\n    position: absolute;\n    top: 0;\n    right: 40px;\n  }\n}\n/*\n * Component: Sidebar\n * ------------------\n */\n.main-sidebar,\n.left-side {\n  position: absolute;\n  top: 0;\n  left: 0;\n  padding-top: 50px;\n  min-height: 100%;\n  width: 230px;\n  z-index: 810;\n  -webkit-transition: -webkit-transform 0.3s ease-in-out, width 0.3s ease-in-out;\n  -moz-transition: -moz-transform 0.3s ease-in-out, width 0.3s ease-in-out;\n  -o-transition: -o-transform 0.3s ease-in-out, width 0.3s ease-in-out;\n  transition: transform 0.3s ease-in-out, width 0.3s ease-in-out;\n}\n@media (max-width: 767px) {\n  .main-sidebar,\n  .left-side {\n    padding-top: 100px;\n  }\n}\n@media (max-width: 767px) {\n  .main-sidebar,\n  .left-side {\n    -webkit-transform: translate(-230px, 0);\n    -ms-transform: translate(-230px, 0);\n    -o-transform: translate(-230px, 0);\n    transform: translate(-230px, 0);\n  }\n}\n@media (min-width: 768px) {\n  .sidebar-collapse .main-sidebar,\n  .sidebar-collapse .left-side {\n    -webkit-transform: translate(-230px, 0);\n    -ms-transform: translate(-230px, 0);\n    -o-transform: translate(-230px, 0);\n    transform: translate(-230px, 0);\n  }\n}\n@media (max-width: 767px) {\n  .sidebar-open .main-sidebar,\n  .sidebar-open .left-side {\n    -webkit-transform: translate(0, 0);\n    -ms-transform: translate(0, 0);\n    -o-transform: translate(0, 0);\n    transform: translate(0, 0);\n  }\n}\n.sidebar {\n  padding-bottom: 10px;\n}\n.sidebar-form input:focus {\n  border-color: transparent;\n}\n.user-panel {\n  position: relative;\n  width: 100%;\n  padding: 10px;\n  overflow: hidden;\n}\n.user-panel:before,\n.user-panel:after {\n  content: \" \";\n  display: table;\n}\n.user-panel:after {\n  clear: both;\n}\n.user-panel > .image > img {\n  width: 100%;\n  max-width: 45px;\n  height: auto;\n}\n.user-panel > .info {\n  padding: 5px 5px 5px 15px;\n  line-height: 1;\n  position: absolute;\n  left: 55px;\n}\n.user-panel > .info > p {\n  font-weight: 600;\n  margin-bottom: 9px;\n}\n.user-panel > .info > a {\n  text-decoration: none;\n  padding-right: 5px;\n  margin-top: 3px;\n  font-size: 11px;\n}\n.user-panel > .info > a > .fa,\n.user-panel > .info > a > .ion,\n.user-panel > .info > a > .glyphicon {\n  margin-right: 3px;\n}\n.sidebar-menu {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.sidebar-menu > li {\n  position: relative;\n  margin: 0;\n  padding: 0;\n}\n.sidebar-menu > li > a {\n  padding: 12px 5px 12px 15px;\n  display: block;\n}\n.sidebar-menu > li > a > .fa,\n.sidebar-menu > li > a > .glyphicon,\n.sidebar-menu > li > a > .ion {\n  width: 20px;\n}\n.sidebar-menu > li .label,\n.sidebar-menu > li .badge {\n  margin-top: 3px;\n  margin-right: 5px;\n}\n.sidebar-menu li.header {\n  padding: 10px 25px 10px 15px;\n  font-size: 12px;\n}\n.sidebar-menu li > a > .fa-angle-left {\n  width: auto;\n  height: auto;\n  padding: 0;\n  margin-right: 10px;\n  margin-top: 3px;\n}\n.sidebar-menu li.active > a > .fa-angle-left {\n  -webkit-transform: rotate(-90deg);\n  -ms-transform: rotate(-90deg);\n  -o-transform: rotate(-90deg);\n  transform: rotate(-90deg);\n}\n.sidebar-menu li.active > .treeview-menu {\n  display: block;\n}\n.sidebar-menu .treeview-menu {\n  display: none;\n  list-style: none;\n  padding: 0;\n  margin: 0;\n  padding-left: 5px;\n}\n.sidebar-menu .treeview-menu .treeview-menu {\n  padding-left: 20px;\n}\n.sidebar-menu .treeview-menu > li {\n  margin: 0;\n}\n.sidebar-menu .treeview-menu > li > a {\n  padding: 5px 5px 5px 15px;\n  display: block;\n  font-size: 14px;\n}\n.sidebar-menu .treeview-menu > li > a > .fa,\n.sidebar-menu .treeview-menu > li > a > .glyphicon,\n.sidebar-menu .treeview-menu > li > a > .ion {\n  width: 20px;\n}\n.sidebar-menu .treeview-menu > li > a > .fa-angle-left,\n.sidebar-menu .treeview-menu > li > a > .fa-angle-down {\n  width: auto;\n}\n/*\n * Component: Sidebar Mini\n */\n@media (min-width: 768px) {\n  .sidebar-mini.sidebar-collapse .content-wrapper,\n  .sidebar-mini.sidebar-collapse .right-side,\n  .sidebar-mini.sidebar-collapse .main-footer {\n    margin-left: 50px !important;\n    z-index: 840;\n  }\n  .sidebar-mini.sidebar-collapse .main-sidebar {\n    -webkit-transform: translate(0, 0);\n    -ms-transform: translate(0, 0);\n    -o-transform: translate(0, 0);\n    transform: translate(0, 0);\n    width: 50px !important;\n    z-index: 850;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li {\n    position: relative;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > a {\n    margin-right: 0;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span {\n    border-top-right-radius: 4px;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li:not(.treeview) > a > span {\n    border-bottom-right-radius: 4px;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    padding-top: 5px;\n    padding-bottom: 5px;\n    border-bottom-right-radius: 4px;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span:not(.pull-right),\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu {\n    display: block !important;\n    position: absolute;\n    width: 180px;\n    left: 50px;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > a > span {\n    top: 0;\n    margin-left: -3px;\n    padding: 12px 5px 12px 20px;\n    background-color: inherit;\n  }\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li:hover > .treeview-menu {\n    top: 44px;\n    margin-left: 0;\n  }\n  .sidebar-mini.sidebar-collapse .main-sidebar .user-panel > .info,\n  .sidebar-mini.sidebar-collapse .sidebar-form,\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > span,\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu,\n  .sidebar-mini.sidebar-collapse .sidebar-menu > li > a > .pull-right,\n  .sidebar-mini.sidebar-collapse .sidebar-menu li.header {\n    display: none !important;\n    -webkit-transform: translateZ(0);\n  }\n  .sidebar-mini.sidebar-collapse .main-header .logo {\n    width: 50px;\n  }\n  .sidebar-mini.sidebar-collapse .main-header .logo > .logo-mini {\n    display: block;\n    margin-left: -15px;\n    margin-right: -15px;\n    font-size: 18px;\n  }\n  .sidebar-mini.sidebar-collapse .main-header .logo > .logo-lg {\n    display: none;\n  }\n  .sidebar-mini.sidebar-collapse .main-header .navbar {\n    margin-left: 50px;\n  }\n}\n.sidebar-menu,\n.main-sidebar .user-panel,\n.sidebar-menu > li.header {\n  white-space: nowrap;\n  overflow: hidden;\n}\n.sidebar-menu:hover {\n  overflow: visible;\n}\n.sidebar-form,\n.sidebar-menu > li.header {\n  overflow: hidden;\n  text-overflow: clip;\n}\n.sidebar-menu li > a {\n  position: relative;\n}\n.sidebar-menu li > a > .pull-right {\n  position: absolute;\n  right: 10px;\n  top: 50%;\n  margin-top: -7px;\n}\n/*\n * Component: Control sidebar. By default, this is the right sidebar.\n */\n.control-sidebar-bg {\n  position: fixed;\n  z-index: 1000;\n  bottom: 0;\n}\n.control-sidebar-bg,\n.control-sidebar {\n  top: 0;\n  right: -230px;\n  width: 230px;\n  -webkit-transition: right 0.3s ease-in-out;\n  -o-transition: right 0.3s ease-in-out;\n  transition: right 0.3s ease-in-out;\n}\n.control-sidebar {\n  position: absolute;\n  padding-top: 50px;\n  z-index: 1010;\n}\n@media (max-width: 768px) {\n  .control-sidebar {\n    padding-top: 100px;\n  }\n}\n.control-sidebar > .tab-content {\n  padding: 10px 15px;\n}\n.control-sidebar.control-sidebar-open,\n.control-sidebar.control-sidebar-open + .control-sidebar-bg {\n  right: 0;\n}\n.control-sidebar-open .control-sidebar-bg,\n.control-sidebar-open .control-sidebar {\n  right: 0;\n}\n@media (min-width: 768px) {\n  .control-sidebar-open .content-wrapper,\n  .control-sidebar-open .right-side,\n  .control-sidebar-open .main-footer {\n    margin-right: 230px;\n  }\n}\n.nav-tabs.control-sidebar-tabs > li:first-of-type > a,\n.nav-tabs.control-sidebar-tabs > li:first-of-type > a:hover,\n.nav-tabs.control-sidebar-tabs > li:first-of-type > a:focus {\n  border-left-width: 0;\n}\n.nav-tabs.control-sidebar-tabs > li > a {\n  border-radius: 0;\n}\n.nav-tabs.control-sidebar-tabs > li > a,\n.nav-tabs.control-sidebar-tabs > li > a:hover {\n  border-top: none;\n  border-right: none;\n  border-left: 1px solid transparent;\n  border-bottom: 1px solid transparent;\n}\n.nav-tabs.control-sidebar-tabs > li > a .icon {\n  font-size: 16px;\n}\n.nav-tabs.control-sidebar-tabs > li.active > a,\n.nav-tabs.control-sidebar-tabs > li.active > a:hover,\n.nav-tabs.control-sidebar-tabs > li.active > a:focus,\n.nav-tabs.control-sidebar-tabs > li.active > a:active {\n  border-top: none;\n  border-right: none;\n  border-bottom: none;\n}\n@media (max-width: 768px) {\n  .nav-tabs.control-sidebar-tabs {\n    display: table;\n  }\n  .nav-tabs.control-sidebar-tabs > li {\n    display: table-cell;\n  }\n}\n.control-sidebar-heading {\n  font-weight: 400;\n  font-size: 16px;\n  padding: 10px 0;\n  margin-bottom: 10px;\n}\n.control-sidebar-subheading {\n  display: block;\n  font-weight: 400;\n  font-size: 14px;\n}\n.control-sidebar-menu {\n  list-style: none;\n  padding: 0;\n  margin: 0 -15px;\n}\n.control-sidebar-menu > li > a {\n  display: block;\n  padding: 10px 15px;\n}\n.control-sidebar-menu > li > a:before,\n.control-sidebar-menu > li > a:after {\n  content: \" \";\n  display: table;\n}\n.control-sidebar-menu > li > a:after {\n  clear: both;\n}\n.control-sidebar-menu > li > a > .control-sidebar-subheading {\n  margin-top: 0;\n}\n.control-sidebar-menu .menu-icon {\n  float: left;\n  width: 35px;\n  height: 35px;\n  border-radius: 50%;\n  text-align: center;\n  line-height: 35px;\n}\n.control-sidebar-menu .menu-info {\n  margin-left: 45px;\n  margin-top: 3px;\n}\n.control-sidebar-menu .menu-info > .control-sidebar-subheading {\n  margin: 0;\n}\n.control-sidebar-menu .menu-info > p {\n  margin: 0;\n  font-size: 11px;\n}\n.control-sidebar-menu .progress {\n  margin: 0;\n}\n.control-sidebar-dark {\n  color: #b8c7ce;\n}\n.control-sidebar-dark,\n.control-sidebar-dark + .control-sidebar-bg {\n  background: #222d32;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs {\n  border-bottom: #1c2529;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a {\n  background: #181f23;\n  color: #b8c7ce;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus {\n  border-left-color: #141a1d;\n  border-bottom-color: #141a1d;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:focus,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:active {\n  background: #1c2529;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li > a:hover {\n  color: #fff;\n}\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:hover,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:focus,\n.control-sidebar-dark .nav-tabs.control-sidebar-tabs > li.active > a:active {\n  background: #222d32;\n  color: #fff;\n}\n.control-sidebar-dark .control-sidebar-heading,\n.control-sidebar-dark .control-sidebar-subheading {\n  color: #fff;\n}\n.control-sidebar-dark .control-sidebar-menu > li > a:hover {\n  background: #1e282c;\n}\n.control-sidebar-dark .control-sidebar-menu > li > a .menu-info > p {\n  color: #b8c7ce;\n}\n.control-sidebar-light {\n  color: #5e5e5e;\n}\n.control-sidebar-light,\n.control-sidebar-light + .control-sidebar-bg {\n  background: #f9fafc;\n  border-left: 1px solid #d2d6de;\n}\n.control-sidebar-light .nav-tabs.control-sidebar-tabs {\n  border-bottom: #d2d6de;\n}\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a {\n  background: #e8ecf4;\n  color: #444444;\n}\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus {\n  border-left-color: #d2d6de;\n  border-bottom-color: #d2d6de;\n}\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:hover,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:focus,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li > a:active {\n  background: #eff1f7;\n}\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:hover,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:focus,\n.control-sidebar-light .nav-tabs.control-sidebar-tabs > li.active > a:active {\n  background: #f9fafc;\n  color: #111;\n}\n.control-sidebar-light .control-sidebar-heading,\n.control-sidebar-light .control-sidebar-subheading {\n  color: #111;\n}\n.control-sidebar-light .control-sidebar-menu {\n  margin-left: -14px;\n}\n.control-sidebar-light .control-sidebar-menu > li > a:hover {\n  background: #f4f4f5;\n}\n.control-sidebar-light .control-sidebar-menu > li > a .menu-info > p {\n  color: #5e5e5e;\n}\n/*\n * Component: Dropdown menus\n * -------------------------\n */\n/*Dropdowns in general*/\n.dropdown-menu {\n  box-shadow: none;\n  border-color: #eee;\n}\n.dropdown-menu > li > a {\n  color: #777;\n}\n.dropdown-menu > li > a > .glyphicon,\n.dropdown-menu > li > a > .fa,\n.dropdown-menu > li > a > .ion {\n  margin-right: 10px;\n}\n.dropdown-menu > li > a:hover {\n  background-color: #e1e3e9;\n  color: #333;\n}\n.dropdown-menu > .divider {\n  background-color: #eee;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu,\n.navbar-nav > .messages-menu > .dropdown-menu,\n.navbar-nav > .tasks-menu > .dropdown-menu {\n  width: 280px;\n  padding: 0 0 0 0;\n  margin: 0;\n  top: 100%;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li,\n.navbar-nav > .messages-menu > .dropdown-menu > li,\n.navbar-nav > .tasks-menu > .dropdown-menu > li {\n  position: relative;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li.header,\n.navbar-nav > .messages-menu > .dropdown-menu > li.header,\n.navbar-nav > .tasks-menu > .dropdown-menu > li.header {\n  border-top-left-radius: 4px;\n  border-top-right-radius: 4px;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n  background-color: #ffffff;\n  padding: 7px 10px;\n  border-bottom: 1px solid #f4f4f4;\n  color: #444444;\n  font-size: 14px;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a,\n.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a,\n.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 4px;\n  border-bottom-left-radius: 4px;\n  font-size: 12px;\n  background-color: #fff;\n  padding: 7px 10px;\n  border-bottom: 1px solid #eeeeee;\n  color: #444 !important;\n  text-align: center;\n}\n@media (max-width: 991px) {\n  .navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a,\n  .navbar-nav > .messages-menu > .dropdown-menu > li.footer > a,\n  .navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a {\n    background: #fff !important;\n    color: #444 !important;\n  }\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li.footer > a:hover,\n.navbar-nav > .messages-menu > .dropdown-menu > li.footer > a:hover,\n.navbar-nav > .tasks-menu > .dropdown-menu > li.footer > a:hover {\n  text-decoration: none;\n  font-weight: normal;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu,\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu,\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu {\n  max-height: 200px;\n  margin: 0;\n  padding: 0;\n  list-style: none;\n  overflow-x: hidden;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a,\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a,\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a {\n  display: block;\n  white-space: nowrap;\n  /* Prevent text from breaking */\n  border-bottom: 1px solid #f4f4f4;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a:hover,\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:hover,\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a:hover {\n  background: #f4f4f4;\n  text-decoration: none;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a {\n  color: #444444;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  padding: 10px;\n}\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .glyphicon,\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .fa,\n.navbar-nav > .notifications-menu > .dropdown-menu > li .menu > li > a > .ion {\n  width: 20px;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a {\n  margin: 0;\n  padding: 10px 10px;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > div > img {\n  margin: auto 10px auto auto;\n  width: 40px;\n  height: 40px;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 {\n  padding: 0;\n  margin: 0 0 0 45px;\n  color: #444444;\n  font-size: 15px;\n  position: relative;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > h4 > small {\n  color: #999999;\n  font-size: 10px;\n  position: absolute;\n  top: 0;\n  right: 0;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a > p {\n  margin: 0 0 0 45px;\n  font-size: 12px;\n  color: #888888;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:before,\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after {\n  content: \" \";\n  display: table;\n}\n.navbar-nav > .messages-menu > .dropdown-menu > li .menu > li > a:after {\n  clear: both;\n}\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a {\n  padding: 10px;\n}\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > h3 {\n  font-size: 14px;\n  padding: 0;\n  margin: 0 0 10px 0;\n  color: #666666;\n}\n.navbar-nav > .tasks-menu > .dropdown-menu > li .menu > li > a > .progress {\n  padding: 0;\n  margin: 0;\n}\n.navbar-nav > .user-menu > .dropdown-menu {\n  border-top-right-radius: 0;\n  border-top-left-radius: 0;\n  padding: 1px 0 0 0;\n  border-top-width: 0;\n  width: 280px;\n}\n.navbar-nav > .user-menu > .dropdown-menu,\n.navbar-nav > .user-menu > .dropdown-menu > .user-body {\n  border-bottom-right-radius: 4px;\n  border-bottom-left-radius: 4px;\n}\n.navbar-nav > .user-menu > .dropdown-menu > li.user-header {\n  height: 175px;\n  padding: 10px;\n  text-align: center;\n}\n.navbar-nav > .user-menu > .dropdown-menu > li.user-header > img {\n  z-index: 5;\n  height: 90px;\n  width: 90px;\n  border: 3px solid;\n  border-color: transparent;\n  border-color: rgba(255, 255, 255, 0.2);\n}\n.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p {\n  z-index: 5;\n  color: #fff;\n  color: rgba(255, 255, 255, 0.8);\n  font-size: 17px;\n  margin-top: 10px;\n}\n.navbar-nav > .user-menu > .dropdown-menu > li.user-header > p > small {\n  display: block;\n  font-size: 12px;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-body {\n  padding: 15px;\n  border-bottom: 1px solid #f4f4f4;\n  border-top: 1px solid #dddddd;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-body:before,\n.navbar-nav > .user-menu > .dropdown-menu > .user-body:after {\n  content: \" \";\n  display: table;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-body:after {\n  clear: both;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-body a {\n  color: #444 !important;\n}\n@media (max-width: 991px) {\n  .navbar-nav > .user-menu > .dropdown-menu > .user-body a {\n    background: #fff !important;\n    color: #444 !important;\n  }\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-footer {\n  background-color: #f9f9f9;\n  padding: 10px;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-footer:before,\n.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after {\n  content: \" \";\n  display: table;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-footer:after {\n  clear: both;\n}\n.navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default {\n  color: #666666;\n}\n@media (max-width: 991px) {\n  .navbar-nav > .user-menu > .dropdown-menu > .user-footer .btn-default:hover {\n    background-color: #f9f9f9;\n  }\n}\n.navbar-nav > .user-menu .user-image {\n  float: left;\n  width: 25px;\n  height: 25px;\n  border-radius: 50%;\n  margin-right: 10px;\n  margin-top: -2px;\n}\n@media (max-width: 767px) {\n  .navbar-nav > .user-menu .user-image {\n    float: none;\n    margin-right: 0;\n    margin-top: -8px;\n    line-height: 10px;\n  }\n}\n/* Add fade animation to dropdown menus by appending\n the class .animated-dropdown-menu to the .dropdown-menu ul (or ol)*/\n.open:not(.dropup) > .animated-dropdown-menu {\n  backface-visibility: visible !important;\n  -webkit-animation: flipInX 0.7s both;\n  -o-animation: flipInX 0.7s both;\n  animation: flipInX 0.7s both;\n}\n@keyframes flipInX {\n  0% {\n    transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    transition-timing-function: ease-in;\n    opacity: 0;\n  }\n  40% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    transition-timing-function: ease-in;\n  }\n  60% {\n    transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n  80% {\n    transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n  100% {\n    transform: perspective(400px);\n  }\n}\n@-webkit-keyframes flipInX {\n  0% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg);\n    -webkit-transition-timing-function: ease-in;\n    opacity: 0;\n  }\n  40% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg);\n    -webkit-transition-timing-function: ease-in;\n  }\n  60% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg);\n    opacity: 1;\n  }\n  80% {\n    -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg);\n  }\n  100% {\n    -webkit-transform: perspective(400px);\n  }\n}\n/* Fix dropdown menu in navbars */\n.navbar-custom-menu > .navbar-nav > li {\n  position: relative;\n}\n.navbar-custom-menu > .navbar-nav > li > .dropdown-menu {\n  position: absolute;\n  right: 0;\n  left: auto;\n}\n@media (max-width: 991px) {\n  .navbar-custom-menu > .navbar-nav {\n    float: right;\n  }\n  .navbar-custom-menu > .navbar-nav > li {\n    position: static;\n  }\n  .navbar-custom-menu > .navbar-nav > li > .dropdown-menu {\n    position: absolute;\n    right: 5%;\n    left: auto;\n    border: 1px solid #ddd;\n    background: #fff;\n  }\n}\n/*\n * Component: Form\n * ---------------\n */\n.form-control {\n  border-radius: 0;\n  box-shadow: none;\n  border-color: #d2d6de;\n}\n.form-control:focus {\n  border-color: #3c8dbc;\n  box-shadow: none;\n}\n.form-control::-moz-placeholder,\n.form-control:-ms-input-placeholder,\n.form-control::-webkit-input-placeholder {\n  color: #bbb;\n  opacity: 1;\n}\n.form-control:not(select) {\n  -webkit-appearance: none;\n  -moz-appearance: none;\n  appearance: none;\n}\n.form-group.has-success label {\n  color: #00a65a;\n}\n.form-group.has-success .form-control {\n  border-color: #00a65a;\n  box-shadow: none;\n}\n.form-group.has-success .help-block {\n  color: #00a65a;\n}\n.form-group.has-warning label {\n  color: #f39c12;\n}\n.form-group.has-warning .form-control {\n  border-color: #f39c12;\n  box-shadow: none;\n}\n.form-group.has-warning .help-block {\n  color: #f39c12;\n}\n.form-group.has-error label {\n  color: #dd4b39;\n}\n.form-group.has-error .form-control {\n  border-color: #dd4b39;\n  box-shadow: none;\n}\n.form-group.has-error .help-block {\n  color: #dd4b39;\n}\n/* Input group */\n.input-group .input-group-addon {\n  border-radius: 0;\n  border-color: #d2d6de;\n  background-color: #fff;\n}\n/* button groups */\n.btn-group-vertical .btn.btn-flat:first-of-type,\n.btn-group-vertical .btn.btn-flat:last-of-type {\n  border-radius: 0;\n}\n.icheck > label {\n  padding-left: 0;\n}\n/* support Font Awesome icons in form-control */\n.form-control-feedback.fa {\n  line-height: 34px;\n}\n.input-lg + .form-control-feedback.fa,\n.input-group-lg + .form-control-feedback.fa,\n.form-group-lg .form-control + .form-control-feedback.fa {\n  line-height: 46px;\n}\n.input-sm + .form-control-feedback.fa,\n.input-group-sm + .form-control-feedback.fa,\n.form-group-sm .form-control + .form-control-feedback.fa {\n  line-height: 30px;\n}\n/*\n * Component: Progress Bar\n * -----------------------\n */\n.progress,\n.progress > .progress-bar {\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.progress,\n.progress > .progress-bar,\n.progress .progress-bar,\n.progress > .progress-bar .progress-bar {\n  border-radius: 1px;\n}\n/* size variation */\n.progress.sm,\n.progress-sm {\n  height: 10px;\n}\n.progress.sm,\n.progress-sm,\n.progress.sm .progress-bar,\n.progress-sm .progress-bar {\n  border-radius: 1px;\n}\n.progress.xs,\n.progress-xs {\n  height: 7px;\n}\n.progress.xs,\n.progress-xs,\n.progress.xs .progress-bar,\n.progress-xs .progress-bar {\n  border-radius: 1px;\n}\n.progress.xxs,\n.progress-xxs {\n  height: 3px;\n}\n.progress.xxs,\n.progress-xxs,\n.progress.xxs .progress-bar,\n.progress-xxs .progress-bar {\n  border-radius: 1px;\n}\n/* Vertical bars */\n.progress.vertical {\n  position: relative;\n  width: 30px;\n  height: 200px;\n  display: inline-block;\n  margin-right: 10px;\n}\n.progress.vertical > .progress-bar {\n  width: 100%;\n  position: absolute;\n  bottom: 0;\n}\n.progress.vertical.sm,\n.progress.vertical.progress-sm {\n  width: 20px;\n}\n.progress.vertical.xs,\n.progress.vertical.progress-xs {\n  width: 10px;\n}\n.progress.vertical.xxs,\n.progress.vertical.progress-xxs {\n  width: 3px;\n}\n.progress-group .progress-text {\n  font-weight: 600;\n}\n.progress-group .progress-number {\n  float: right;\n}\n/* Remove margins from progress bars when put in a table */\n.table tr > td .progress {\n  margin: 0;\n}\n.progress-bar-light-blue,\n.progress-bar-primary {\n  background-color: #3c8dbc;\n}\n.progress-striped .progress-bar-light-blue,\n.progress-striped .progress-bar-primary {\n  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);\n  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);\n  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);\n}\n.progress-bar-green,\n.progress-bar-success {\n  background-color: #00a65a;\n}\n.progress-striped .progress-bar-green,\n.progress-striped .progress-bar-success {\n  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);\n  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);\n  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);\n}\n.progress-bar-aqua,\n.progress-bar-info {\n  background-color: #00c0ef;\n}\n.progress-striped .progress-bar-aqua,\n.progress-striped .progress-bar-info {\n  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);\n  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);\n  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);\n}\n.progress-bar-yellow,\n.progress-bar-warning {\n  background-color: #f39c12;\n}\n.progress-striped .progress-bar-yellow,\n.progress-striped .progress-bar-warning {\n  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);\n  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);\n  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);\n}\n.progress-bar-red,\n.progress-bar-danger {\n  background-color: #dd4b39;\n}\n.progress-striped .progress-bar-red,\n.progress-striped .progress-bar-danger {\n  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);\n  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);\n  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);\n}\n/*\n * Component: Small Box\n * --------------------\n */\n.small-box {\n  border-radius: 2px;\n  position: relative;\n  display: block;\n  margin-bottom: 20px;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n}\n.small-box > .inner {\n  padding: 10px;\n}\n.small-box > .small-box-footer {\n  position: relative;\n  text-align: center;\n  padding: 3px 0;\n  color: #fff;\n  color: rgba(255, 255, 255, 0.8);\n  display: block;\n  z-index: 10;\n  background: rgba(0, 0, 0, 0.1);\n  text-decoration: none;\n}\n.small-box > .small-box-footer:hover {\n  color: #fff;\n  background: rgba(0, 0, 0, 0.15);\n}\n.small-box h3 {\n  font-size: 38px;\n  font-weight: bold;\n  margin: 0 0 10px 0;\n  white-space: nowrap;\n  padding: 0;\n}\n.small-box p {\n  font-size: 15px;\n}\n.small-box p > small {\n  display: block;\n  color: #f9f9f9;\n  font-size: 13px;\n  margin-top: 5px;\n}\n.small-box h3,\n.small-box p {\n  z-index: 5;\n}\n.small-box .icon {\n  -webkit-transition: all 0.3s linear;\n  -o-transition: all 0.3s linear;\n  transition: all 0.3s linear;\n  position: absolute;\n  top: -10px;\n  right: 10px;\n  z-index: 0;\n  font-size: 90px;\n  color: rgba(0, 0, 0, 0.15);\n}\n.small-box:hover {\n  text-decoration: none;\n  color: #f9f9f9;\n}\n.small-box:hover .icon {\n  font-size: 95px;\n}\n@media (max-width: 767px) {\n  .small-box {\n    text-align: center;\n  }\n  .small-box .icon {\n    display: none;\n  }\n  .small-box p {\n    font-size: 12px;\n  }\n}\n/*\n * Component: Box\n * --------------\n */\n.box {\n  position: relative;\n  border-radius: 3px;\n  background: #ffffff;\n  border-top: 3px solid #d2d6de;\n  margin-bottom: 20px;\n  width: 100%;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n}\n.box.box-primary {\n  border-top-color: #3c8dbc;\n}\n.box.box-info {\n  border-top-color: #00c0ef;\n}\n.box.box-danger {\n  border-top-color: #dd4b39;\n}\n.box.box-warning {\n  border-top-color: #f39c12;\n}\n.box.box-success {\n  border-top-color: #00a65a;\n}\n.box.box-default {\n  border-top-color: #d2d6de;\n}\n.box.collapsed-box .box-body,\n.box.collapsed-box .box-footer {\n  display: none;\n}\n.box .nav-stacked > li {\n  border-bottom: 1px solid #f4f4f4;\n  margin: 0;\n}\n.box .nav-stacked > li:last-of-type {\n  border-bottom: none;\n}\n.box.height-control .box-body {\n  max-height: 300px;\n  overflow: auto;\n}\n.box .border-right {\n  border-right: 1px solid #f4f4f4;\n}\n.box .border-left {\n  border-left: 1px solid #f4f4f4;\n}\n.box.box-solid {\n  border-top: 0;\n}\n.box.box-solid > .box-header .btn.btn-default {\n  background: transparent;\n}\n.box.box-solid > .box-header .btn:hover,\n.box.box-solid > .box-header a:hover {\n  background: rgba(0, 0, 0, 0.1);\n}\n.box.box-solid.box-default {\n  border: 1px solid #d2d6de;\n}\n.box.box-solid.box-default > .box-header {\n  color: #444444;\n  background: #d2d6de;\n  background-color: #d2d6de;\n}\n.box.box-solid.box-default > .box-header a,\n.box.box-solid.box-default > .box-header .btn {\n  color: #444444;\n}\n.box.box-solid.box-primary {\n  border: 1px solid #3c8dbc;\n}\n.box.box-solid.box-primary > .box-header {\n  color: #ffffff;\n  background: #3c8dbc;\n  background-color: #3c8dbc;\n}\n.box.box-solid.box-primary > .box-header a,\n.box.box-solid.box-primary > .box-header .btn {\n  color: #ffffff;\n}\n.box.box-solid.box-info {\n  border: 1px solid #00c0ef;\n}\n.box.box-solid.box-info > .box-header {\n  color: #ffffff;\n  background: #00c0ef;\n  background-color: #00c0ef;\n}\n.box.box-solid.box-info > .box-header a,\n.box.box-solid.box-info > .box-header .btn {\n  color: #ffffff;\n}\n.box.box-solid.box-danger {\n  border: 1px solid #dd4b39;\n}\n.box.box-solid.box-danger > .box-header {\n  color: #ffffff;\n  background: #dd4b39;\n  background-color: #dd4b39;\n}\n.box.box-solid.box-danger > .box-header a,\n.box.box-solid.box-danger > .box-header .btn {\n  color: #ffffff;\n}\n.box.box-solid.box-warning {\n  border: 1px solid #f39c12;\n}\n.box.box-solid.box-warning > .box-header {\n  color: #ffffff;\n  background: #f39c12;\n  background-color: #f39c12;\n}\n.box.box-solid.box-warning > .box-header a,\n.box.box-solid.box-warning > .box-header .btn {\n  color: #ffffff;\n}\n.box.box-solid.box-success {\n  border: 1px solid #00a65a;\n}\n.box.box-solid.box-success > .box-header {\n  color: #ffffff;\n  background: #00a65a;\n  background-color: #00a65a;\n}\n.box.box-solid.box-success > .box-header a,\n.box.box-solid.box-success > .box-header .btn {\n  color: #ffffff;\n}\n.box.box-solid > .box-header > .box-tools .btn {\n  border: 0;\n  box-shadow: none;\n}\n.box.box-solid[class*='bg'] > .box-header {\n  color: #fff;\n}\n.box .box-group > .box {\n  margin-bottom: 5px;\n}\n.box .knob-label {\n  text-align: center;\n  color: #333;\n  font-weight: 100;\n  font-size: 12px;\n  margin-bottom: 0.3em;\n}\n.box > .overlay,\n.overlay-wrapper > .overlay,\n.box > .loading-img,\n.overlay-wrapper > .loading-img {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n}\n.box .overlay,\n.overlay-wrapper .overlay {\n  z-index: 50;\n  background: rgba(255, 255, 255, 0.7);\n  border-radius: 3px;\n}\n.box .overlay > .fa,\n.overlay-wrapper .overlay > .fa {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  margin-left: -15px;\n  margin-top: -15px;\n  color: #000;\n  font-size: 30px;\n}\n.box .overlay.dark,\n.overlay-wrapper .overlay.dark {\n  background: rgba(0, 0, 0, 0.5);\n}\n.box-header:before,\n.box-body:before,\n.box-footer:before,\n.box-header:after,\n.box-body:after,\n.box-footer:after {\n  content: \" \";\n  display: table;\n}\n.box-header:after,\n.box-body:after,\n.box-footer:after {\n  clear: both;\n}\n.box-header {\n  color: #444;\n  display: block;\n  padding: 10px;\n  position: relative;\n}\n.box-header.with-border {\n  border-bottom: 1px solid #f4f4f4;\n}\n.collapsed-box .box-header.with-border {\n  border-bottom: none;\n}\n.box-header > .fa,\n.box-header > .glyphicon,\n.box-header > .ion,\n.box-header .box-title {\n  display: inline-block;\n  font-size: 18px;\n  margin: 0;\n  line-height: 1;\n}\n.box-header > .fa,\n.box-header > .glyphicon,\n.box-header > .ion {\n  margin-right: 5px;\n}\n.box-header > .box-tools {\n  position: absolute;\n  right: 10px;\n  top: 5px;\n}\n.box-header > .box-tools [data-toggle=\"tooltip\"] {\n  position: relative;\n}\n.box-header > .box-tools.pull-right .dropdown-menu {\n  right: 0;\n  left: auto;\n}\n.btn-box-tool {\n  padding: 5px;\n  font-size: 12px;\n  background: transparent;\n  color: #97a0b3;\n}\n.open .btn-box-tool,\n.btn-box-tool:hover {\n  color: #606c84;\n}\n.btn-box-tool.btn:active {\n  box-shadow: none;\n}\n.box-body {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n  padding: 10px;\n}\n.no-header .box-body {\n  border-top-right-radius: 3px;\n  border-top-left-radius: 3px;\n}\n.box-body > .table {\n  margin-bottom: 0;\n}\n.box-body .fc {\n  margin-top: 5px;\n}\n.box-body .full-width-chart {\n  margin: -19px;\n}\n.box-body.no-padding .full-width-chart {\n  margin: -9px;\n}\n.box-body .box-pane {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 3px;\n}\n.box-body .box-pane-right {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 0;\n}\n.box-footer {\n  border-top-left-radius: 0;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n  border-top: 1px solid #f4f4f4;\n  padding: 10px;\n  background-color: #ffffff;\n}\n.chart-legend {\n  margin: 10px 0;\n}\n@media (max-width: 991px) {\n  .chart-legend > li {\n    float: left;\n    margin-right: 10px;\n  }\n}\n.box-comments {\n  background: #f7f7f7;\n}\n.box-comments .box-comment {\n  padding: 8px 0;\n  border-bottom: 1px solid #eee;\n}\n.box-comments .box-comment:before,\n.box-comments .box-comment:after {\n  content: \" \";\n  display: table;\n}\n.box-comments .box-comment:after {\n  clear: both;\n}\n.box-comments .box-comment:last-of-type {\n  border-bottom: 0;\n}\n.box-comments .box-comment:first-of-type {\n  padding-top: 0;\n}\n.box-comments .box-comment img {\n  float: left;\n}\n.box-comments .comment-text {\n  margin-left: 40px;\n  color: #555;\n}\n.box-comments .username {\n  color: #444;\n  display: block;\n  font-weight: 600;\n}\n.box-comments .text-muted {\n  font-weight: 400;\n  font-size: 12px;\n}\n/* Widget: TODO LIST */\n.todo-list {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n  overflow: auto;\n}\n.todo-list > li {\n  border-radius: 2px;\n  padding: 10px;\n  background: #f4f4f4;\n  margin-bottom: 2px;\n  border-left: 2px solid #e6e7e8;\n  color: #444;\n}\n.todo-list > li:last-of-type {\n  margin-bottom: 0;\n}\n.todo-list > li > input[type='checkbox'] {\n  margin: 0 10px 0 5px;\n}\n.todo-list > li .text {\n  display: inline-block;\n  margin-left: 5px;\n  font-weight: 600;\n}\n.todo-list > li .label {\n  margin-left: 10px;\n  font-size: 9px;\n}\n.todo-list > li .tools {\n  display: none;\n  float: right;\n  color: #dd4b39;\n}\n.todo-list > li .tools > .fa,\n.todo-list > li .tools > .glyphicon,\n.todo-list > li .tools > .ion {\n  margin-right: 5px;\n  cursor: pointer;\n}\n.todo-list > li:hover .tools {\n  display: inline-block;\n}\n.todo-list > li.done {\n  color: #999;\n}\n.todo-list > li.done .text {\n  text-decoration: line-through;\n  font-weight: 500;\n}\n.todo-list > li.done .label {\n  background: #d2d6de !important;\n}\n.todo-list .danger {\n  border-left-color: #dd4b39;\n}\n.todo-list .warning {\n  border-left-color: #f39c12;\n}\n.todo-list .info {\n  border-left-color: #00c0ef;\n}\n.todo-list .success {\n  border-left-color: #00a65a;\n}\n.todo-list .primary {\n  border-left-color: #3c8dbc;\n}\n.todo-list .handle {\n  display: inline-block;\n  cursor: move;\n  margin: 0 5px;\n}\n/* Chat widget (DEPRECATED - this will be removed in the next major release. Use Direct Chat instead)*/\n.chat {\n  padding: 5px 20px 5px 10px;\n}\n.chat .item {\n  margin-bottom: 10px;\n}\n.chat .item:before,\n.chat .item:after {\n  content: \" \";\n  display: table;\n}\n.chat .item:after {\n  clear: both;\n}\n.chat .item > img {\n  width: 40px;\n  height: 40px;\n  border: 2px solid transparent;\n  border-radius: 50%;\n}\n.chat .item > .online {\n  border: 2px solid #00a65a;\n}\n.chat .item > .offline {\n  border: 2px solid #dd4b39;\n}\n.chat .item > .message {\n  margin-left: 55px;\n  margin-top: -40px;\n}\n.chat .item > .message > .name {\n  display: block;\n  font-weight: 600;\n}\n.chat .item > .attachment {\n  border-radius: 3px;\n  background: #f4f4f4;\n  margin-left: 65px;\n  margin-right: 15px;\n  padding: 10px;\n}\n.chat .item > .attachment > h4 {\n  margin: 0 0 5px 0;\n  font-weight: 600;\n  font-size: 14px;\n}\n.chat .item > .attachment > p,\n.chat .item > .attachment > .filename {\n  font-weight: 600;\n  font-size: 13px;\n  font-style: italic;\n  margin: 0;\n}\n.chat .item > .attachment:before,\n.chat .item > .attachment:after {\n  content: \" \";\n  display: table;\n}\n.chat .item > .attachment:after {\n  clear: both;\n}\n.box-input {\n  max-width: 200px;\n}\n.modal .panel-body {\n  color: #444;\n}\n/*\n * Component: Info Box\n * -------------------\n */\n.info-box {\n  display: block;\n  min-height: 90px;\n  background: #fff;\n  width: 100%;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  border-radius: 2px;\n  margin-bottom: 15px;\n}\n.info-box small {\n  font-size: 14px;\n}\n.info-box .progress {\n  background: rgba(0, 0, 0, 0.2);\n  margin: 5px -10px 5px -10px;\n  height: 2px;\n}\n.info-box .progress,\n.info-box .progress .progress-bar {\n  border-radius: 0;\n}\n.info-box .progress .progress-bar {\n  background: #fff;\n}\n.info-box-icon {\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n  display: block;\n  float: left;\n  height: 90px;\n  width: 90px;\n  text-align: center;\n  font-size: 45px;\n  line-height: 90px;\n  background: rgba(0, 0, 0, 0.2);\n}\n.info-box-icon > img {\n  max-width: 100%;\n}\n.info-box-content {\n  padding: 5px 10px;\n  margin-left: 90px;\n}\n.info-box-number {\n  display: block;\n  font-weight: bold;\n  font-size: 18px;\n}\n.progress-description,\n.info-box-text {\n  display: block;\n  font-size: 14px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.info-box-text {\n  text-transform: uppercase;\n}\n.info-box-more {\n  display: block;\n}\n.progress-description {\n  margin: 0;\n}\n/*\n * Component: Timeline\n * -------------------\n */\n.timeline {\n  position: relative;\n  margin: 0 0 30px 0;\n  padding: 0;\n  list-style: none;\n}\n.timeline:before {\n  content: '';\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  width: 4px;\n  background: #ddd;\n  left: 31px;\n  margin: 0;\n  border-radius: 2px;\n}\n.timeline > li {\n  position: relative;\n  margin-right: 10px;\n  margin-bottom: 15px;\n}\n.timeline > li:before,\n.timeline > li:after {\n  content: \" \";\n  display: table;\n}\n.timeline > li:after {\n  clear: both;\n}\n.timeline > li > .timeline-item {\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  border-radius: 3px;\n  margin-top: 0;\n  background: #fff;\n  color: #444;\n  margin-left: 60px;\n  margin-right: 15px;\n  padding: 0;\n  position: relative;\n}\n.timeline > li > .timeline-item > .time {\n  color: #999;\n  float: right;\n  padding: 10px;\n  font-size: 12px;\n}\n.timeline > li > .timeline-item > .timeline-header {\n  margin: 0;\n  color: #555;\n  border-bottom: 1px solid #f4f4f4;\n  padding: 10px;\n  font-size: 16px;\n  line-height: 1.1;\n}\n.timeline > li > .timeline-item > .timeline-header > a {\n  font-weight: 600;\n}\n.timeline > li > .timeline-item > .timeline-body,\n.timeline > li > .timeline-item > .timeline-footer {\n  padding: 10px;\n}\n.timeline > li > .fa,\n.timeline > li > .glyphicon,\n.timeline > li > .ion {\n  width: 30px;\n  height: 30px;\n  font-size: 15px;\n  line-height: 30px;\n  position: absolute;\n  color: #666;\n  background: #d2d6de;\n  border-radius: 50%;\n  text-align: center;\n  left: 18px;\n  top: 0;\n}\n.timeline > .time-label > span {\n  font-weight: 600;\n  padding: 5px;\n  display: inline-block;\n  background-color: #fff;\n  border-radius: 4px;\n}\n.timeline-inverse > li > .timeline-item {\n  background: #f0f0f0;\n  border: 1px solid #ddd;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.timeline-inverse > li > .timeline-item > .timeline-header {\n  border-bottom-color: #ddd;\n}\n/*\n * Component: Button\n * -----------------\n */\n.btn {\n  border-radius: 3px;\n  -webkit-box-shadow: none;\n  box-shadow: none;\n  border: 1px solid transparent;\n}\n.btn.uppercase {\n  text-transform: uppercase;\n}\n.btn.btn-flat {\n  border-radius: 0;\n  -webkit-box-shadow: none;\n  -moz-box-shadow: none;\n  box-shadow: none;\n  border-width: 1px;\n}\n.btn:active {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn:focus {\n  outline: none;\n}\n.btn.btn-file {\n  position: relative;\n  overflow: hidden;\n}\n.btn.btn-file > input[type='file'] {\n  position: absolute;\n  top: 0;\n  right: 0;\n  min-width: 100%;\n  min-height: 100%;\n  font-size: 100px;\n  text-align: right;\n  opacity: 0;\n  filter: alpha(opacity=0);\n  outline: none;\n  background: white;\n  cursor: inherit;\n  display: block;\n}\n.btn-default {\n  background-color: #f4f4f4;\n  color: #444;\n  border-color: #ddd;\n}\n.btn-default:hover,\n.btn-default:active,\n.btn-default.hover {\n  background-color: #e7e7e7;\n}\n.btn-primary {\n  background-color: #3c8dbc;\n  border-color: #367fa9;\n}\n.btn-primary:hover,\n.btn-primary:active,\n.btn-primary.hover {\n  background-color: #367fa9;\n}\n.btn-success {\n  background-color: #00a65a;\n  border-color: #008d4c;\n}\n.btn-success:hover,\n.btn-success:active,\n.btn-success.hover {\n  background-color: #008d4c;\n}\n.btn-info {\n  background-color: #00c0ef;\n  border-color: #00acd6;\n}\n.btn-info:hover,\n.btn-info:active,\n.btn-info.hover {\n  background-color: #00acd6;\n}\n.btn-danger {\n  background-color: #dd4b39;\n  border-color: #d73925;\n}\n.btn-danger:hover,\n.btn-danger:active,\n.btn-danger.hover {\n  background-color: #d73925;\n}\n.btn-warning {\n  background-color: #f39c12;\n  border-color: #e08e0b;\n}\n.btn-warning:hover,\n.btn-warning:active,\n.btn-warning.hover {\n  background-color: #e08e0b;\n}\n.btn-outline {\n  border: 1px solid #fff;\n  background: transparent;\n  color: #fff;\n}\n.btn-outline:hover,\n.btn-outline:focus,\n.btn-outline:active {\n  color: rgba(255, 255, 255, 0.7);\n  border-color: rgba(255, 255, 255, 0.7);\n}\n.btn-link {\n  -webkit-box-shadow: none;\n  box-shadow: none;\n}\n.btn[class*='bg-']:hover {\n  -webkit-box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2);\n  box-shadow: inset 0 0 100px rgba(0, 0, 0, 0.2);\n}\n.btn-app {\n  border-radius: 3px;\n  position: relative;\n  padding: 15px 5px;\n  margin: 0 0 10px 10px;\n  min-width: 80px;\n  height: 60px;\n  text-align: center;\n  color: #666;\n  border: 1px solid #ddd;\n  background-color: #f4f4f4;\n  font-size: 12px;\n}\n.btn-app > .fa,\n.btn-app > .glyphicon,\n.btn-app > .ion {\n  font-size: 20px;\n  display: block;\n}\n.btn-app:hover {\n  background: #f4f4f4;\n  color: #444;\n  border-color: #aaa;\n}\n.btn-app:active,\n.btn-app:focus {\n  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  -moz-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-app > .badge {\n  position: absolute;\n  top: -3px;\n  right: -10px;\n  font-size: 10px;\n  font-weight: 400;\n}\n/*\n * Component: Callout\n * ------------------\n */\n.callout {\n  border-radius: 3px;\n  margin: 0 0 20px 0;\n  padding: 15px 30px 15px 15px;\n  border-left: 5px solid #eee;\n}\n.callout a {\n  color: #fff;\n  text-decoration: underline;\n}\n.callout a:hover {\n  color: #eee;\n}\n.callout h4 {\n  margin-top: 0;\n  font-weight: 600;\n}\n.callout p:last-child {\n  margin-bottom: 0;\n}\n.callout code,\n.callout .highlight {\n  background-color: #fff;\n}\n.callout.callout-danger {\n  border-color: #c23321;\n}\n.callout.callout-warning {\n  border-color: #c87f0a;\n}\n.callout.callout-info {\n  border-color: #0097bc;\n}\n.callout.callout-success {\n  border-color: #00733e;\n}\n/*\n * Component: alert\n * ----------------\n */\n.alert {\n  border-radius: 3px;\n}\n.alert h4 {\n  font-weight: 600;\n}\n.alert .icon {\n  margin-right: 10px;\n}\n.alert .close {\n  color: #000;\n  opacity: 0.2;\n  filter: alpha(opacity=20);\n}\n.alert .close:hover {\n  opacity: 0.5;\n  filter: alpha(opacity=50);\n}\n.alert a {\n  color: #fff;\n  text-decoration: underline;\n}\n.alert-success {\n  border-color: #008d4c;\n}\n.alert-danger,\n.alert-error {\n  border-color: #d73925;\n}\n.alert-warning {\n  border-color: #e08e0b;\n}\n.alert-info {\n  border-color: #00acd6;\n}\n/*\n * Component: Nav\n * --------------\n */\n.nav > li > a:hover,\n.nav > li > a:active,\n.nav > li > a:focus {\n  color: #444;\n  background: #f7f7f7;\n}\n/* NAV PILLS */\n.nav-pills > li > a {\n  border-radius: 0;\n  border-top: 3px solid transparent;\n  color: #444;\n}\n.nav-pills > li > a > .fa,\n.nav-pills > li > a > .glyphicon,\n.nav-pills > li > a > .ion {\n  margin-right: 5px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n  border-top-color: #3c8dbc;\n}\n.nav-pills > li.active > a {\n  font-weight: 600;\n}\n/* NAV STACKED */\n.nav-stacked > li > a {\n  border-radius: 0;\n  border-top: 0;\n  border-left: 3px solid transparent;\n  color: #444;\n}\n.nav-stacked > li.active > a,\n.nav-stacked > li.active > a:hover {\n  background: transparent;\n  color: #444;\n  border-top: 0;\n  border-left-color: #3c8dbc;\n}\n.nav-stacked > li.header {\n  border-bottom: 1px solid #ddd;\n  color: #777;\n  margin-bottom: 10px;\n  padding: 5px 10px;\n  text-transform: uppercase;\n}\n/* NAV TABS */\n.nav-tabs-custom {\n  margin-bottom: 20px;\n  background: #fff;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  border-radius: 3px;\n}\n.nav-tabs-custom > .nav-tabs {\n  margin: 0;\n  border-bottom-color: #f4f4f4;\n  border-top-right-radius: 3px;\n  border-top-left-radius: 3px;\n}\n.nav-tabs-custom > .nav-tabs > li {\n  border-top: 3px solid transparent;\n  margin-bottom: -2px;\n  margin-right: 5px;\n}\n.nav-tabs-custom > .nav-tabs > li > a {\n  color: #444;\n  border-radius: 0;\n}\n.nav-tabs-custom > .nav-tabs > li > a.text-muted {\n  color: #999;\n}\n.nav-tabs-custom > .nav-tabs > li > a,\n.nav-tabs-custom > .nav-tabs > li > a:hover {\n  background: transparent;\n  margin: 0;\n}\n.nav-tabs-custom > .nav-tabs > li > a:hover {\n  color: #999;\n}\n.nav-tabs-custom > .nav-tabs > li:not(.active) > a:hover,\n.nav-tabs-custom > .nav-tabs > li:not(.active) > a:focus,\n.nav-tabs-custom > .nav-tabs > li:not(.active) > a:active {\n  border-color: transparent;\n}\n.nav-tabs-custom > .nav-tabs > li.active {\n  border-top-color: #3c8dbc;\n}\n.nav-tabs-custom > .nav-tabs > li.active > a,\n.nav-tabs-custom > .nav-tabs > li.active:hover > a {\n  background-color: #fff;\n  color: #444;\n}\n.nav-tabs-custom > .nav-tabs > li.active > a {\n  border-top-color: transparent;\n  border-left-color: #f4f4f4;\n  border-right-color: #f4f4f4;\n}\n.nav-tabs-custom > .nav-tabs > li:first-of-type {\n  margin-left: 0;\n}\n.nav-tabs-custom > .nav-tabs > li:first-of-type.active > a {\n  border-left-color: transparent;\n}\n.nav-tabs-custom > .nav-tabs.pull-right {\n  float: none !important;\n}\n.nav-tabs-custom > .nav-tabs.pull-right > li {\n  float: right;\n}\n.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type {\n  margin-right: 0;\n}\n.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type > a {\n  border-left-width: 1px;\n}\n.nav-tabs-custom > .nav-tabs.pull-right > li:first-of-type.active > a {\n  border-left-color: #f4f4f4;\n  border-right-color: transparent;\n}\n.nav-tabs-custom > .nav-tabs > li.header {\n  line-height: 35px;\n  padding: 0 10px;\n  font-size: 20px;\n  color: #444;\n}\n.nav-tabs-custom > .nav-tabs > li.header > .fa,\n.nav-tabs-custom > .nav-tabs > li.header > .glyphicon,\n.nav-tabs-custom > .nav-tabs > li.header > .ion {\n  margin-right: 5px;\n}\n.nav-tabs-custom > .tab-content {\n  background: #fff;\n  padding: 10px;\n  border-bottom-right-radius: 3px;\n  border-bottom-left-radius: 3px;\n}\n.nav-tabs-custom .dropdown.open > a:active,\n.nav-tabs-custom .dropdown.open > a:focus {\n  background: transparent;\n  color: #999;\n}\n.nav-tabs-custom.tab-primary > .nav-tabs > li.active {\n  border-top-color: #3c8dbc;\n}\n.nav-tabs-custom.tab-info > .nav-tabs > li.active {\n  border-top-color: #00c0ef;\n}\n.nav-tabs-custom.tab-danger > .nav-tabs > li.active {\n  border-top-color: #dd4b39;\n}\n.nav-tabs-custom.tab-warning > .nav-tabs > li.active {\n  border-top-color: #f39c12;\n}\n.nav-tabs-custom.tab-success > .nav-tabs > li.active {\n  border-top-color: #00a65a;\n}\n.nav-tabs-custom.tab-default > .nav-tabs > li.active {\n  border-top-color: #d2d6de;\n}\n/* PAGINATION */\n.pagination > li > a {\n  background: #fafafa;\n  color: #666;\n}\n.pagination.pagination-flat > li > a {\n  border-radius: 0 !important;\n}\n/*\n * Component: Products List\n * ------------------------\n */\n.products-list {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.products-list > .item {\n  border-radius: 3px;\n  -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  padding: 10px 0;\n  background: #fff;\n}\n.products-list > .item:before,\n.products-list > .item:after {\n  content: \" \";\n  display: table;\n}\n.products-list > .item:after {\n  clear: both;\n}\n.products-list .product-img {\n  float: left;\n}\n.products-list .product-img img {\n  width: 50px;\n  height: 50px;\n}\n.products-list .product-info {\n  margin-left: 60px;\n}\n.products-list .product-title {\n  font-weight: 600;\n}\n.products-list .product-description {\n  display: block;\n  color: #999;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n.product-list-in-box > .item {\n  -webkit-box-shadow: none;\n  box-shadow: none;\n  border-radius: 0;\n  border-bottom: 1px solid #f4f4f4;\n}\n.product-list-in-box > .item:last-of-type {\n  border-bottom-width: 0;\n}\n/*\n * Component: Table\n * ----------------\n */\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n  border-top: 1px solid #f4f4f4;\n}\n.table > thead > tr > th {\n  border-bottom: 2px solid #f4f4f4;\n}\n.table tr td .progress {\n  margin-top: 5px;\n}\n.table-bordered {\n  border: 1px solid #f4f4f4;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n  border: 1px solid #f4f4f4;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n  border-bottom-width: 2px;\n}\n.table.no-border,\n.table.no-border td,\n.table.no-border th {\n  border: 0;\n}\n/* .text-center in tables */\ntable.text-center,\ntable.text-center td,\ntable.text-center th {\n  text-align: center;\n}\n.table.align th {\n  text-align: left;\n}\n.table.align td {\n  text-align: right;\n}\n/*\n * Component: Label\n * ----------------\n */\n.label-default {\n  background-color: #d2d6de;\n  color: #444;\n}\n/*\n * Component: Direct Chat\n * ----------------------\n */\n.direct-chat .box-body {\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 0;\n  position: relative;\n  overflow-x: hidden;\n  padding: 0;\n}\n.direct-chat.chat-pane-open .direct-chat-contacts {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.direct-chat-messages {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n  padding: 10px;\n  height: 250px;\n  overflow: auto;\n}\n.direct-chat-msg,\n.direct-chat-text {\n  display: block;\n}\n.direct-chat-msg {\n  margin-bottom: 10px;\n}\n.direct-chat-msg:before,\n.direct-chat-msg:after {\n  content: \" \";\n  display: table;\n}\n.direct-chat-msg:after {\n  clear: both;\n}\n.direct-chat-messages,\n.direct-chat-contacts {\n  -webkit-transition: -webkit-transform 0.5s ease-in-out;\n  -moz-transition: -moz-transform 0.5s ease-in-out;\n  -o-transition: -o-transform 0.5s ease-in-out;\n  transition: transform 0.5s ease-in-out;\n}\n.direct-chat-text {\n  border-radius: 5px;\n  position: relative;\n  padding: 5px 10px;\n  background: #d2d6de;\n  border: 1px solid #d2d6de;\n  margin: 5px 0 0 50px;\n  color: #444444;\n}\n.direct-chat-text:after,\n.direct-chat-text:before {\n  position: absolute;\n  right: 100%;\n  top: 15px;\n  border: solid transparent;\n  border-right-color: #d2d6de;\n  content: ' ';\n  height: 0;\n  width: 0;\n  pointer-events: none;\n}\n.direct-chat-text:after {\n  border-width: 5px;\n  margin-top: -5px;\n}\n.direct-chat-text:before {\n  border-width: 6px;\n  margin-top: -6px;\n}\n.right .direct-chat-text {\n  margin-right: 50px;\n  margin-left: 0;\n}\n.right .direct-chat-text:after,\n.right .direct-chat-text:before {\n  right: auto;\n  left: 100%;\n  border-right-color: transparent;\n  border-left-color: #d2d6de;\n}\n.direct-chat-img {\n  border-radius: 50%;\n  float: left;\n  width: 40px;\n  height: 40px;\n}\n.right .direct-chat-img {\n  float: right;\n}\n.direct-chat-info {\n  display: block;\n  margin-bottom: 2px;\n  font-size: 12px;\n}\n.direct-chat-name {\n  font-weight: 600;\n}\n.direct-chat-timestamp {\n  color: #999;\n}\n.direct-chat-contacts-open .direct-chat-contacts {\n  -webkit-transform: translate(0, 0);\n  -ms-transform: translate(0, 0);\n  -o-transform: translate(0, 0);\n  transform: translate(0, 0);\n}\n.direct-chat-contacts {\n  -webkit-transform: translate(101%, 0);\n  -ms-transform: translate(101%, 0);\n  -o-transform: translate(101%, 0);\n  transform: translate(101%, 0);\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  height: 250px;\n  width: 100%;\n  background: #222d32;\n  color: #fff;\n  overflow: auto;\n}\n.contacts-list > li {\n  border-bottom: 1px solid rgba(0, 0, 0, 0.2);\n  padding: 10px;\n  margin: 0;\n}\n.contacts-list > li:before,\n.contacts-list > li:after {\n  content: \" \";\n  display: table;\n}\n.contacts-list > li:after {\n  clear: both;\n}\n.contacts-list > li:last-of-type {\n  border-bottom: none;\n}\n.contacts-list-img {\n  border-radius: 50%;\n  width: 40px;\n  float: left;\n}\n.contacts-list-info {\n  margin-left: 45px;\n  color: #fff;\n}\n.contacts-list-name,\n.contacts-list-status {\n  display: block;\n}\n.contacts-list-name {\n  font-weight: 600;\n}\n.contacts-list-status {\n  font-size: 12px;\n}\n.contacts-list-date {\n  color: #aaa;\n  font-weight: normal;\n}\n.contacts-list-msg {\n  color: #999;\n}\n.direct-chat-danger .right > .direct-chat-text {\n  background: #dd4b39;\n  border-color: #dd4b39;\n  color: #ffffff;\n}\n.direct-chat-danger .right > .direct-chat-text:after,\n.direct-chat-danger .right > .direct-chat-text:before {\n  border-left-color: #dd4b39;\n}\n.direct-chat-primary .right > .direct-chat-text {\n  background: #3c8dbc;\n  border-color: #3c8dbc;\n  color: #ffffff;\n}\n.direct-chat-primary .right > .direct-chat-text:after,\n.direct-chat-primary .right > .direct-chat-text:before {\n  border-left-color: #3c8dbc;\n}\n.direct-chat-warning .right > .direct-chat-text {\n  background: #f39c12;\n  border-color: #f39c12;\n  color: #ffffff;\n}\n.direct-chat-warning .right > .direct-chat-text:after,\n.direct-chat-warning .right > .direct-chat-text:before {\n  border-left-color: #f39c12;\n}\n.direct-chat-info .right > .direct-chat-text {\n  background: #00c0ef;\n  border-color: #00c0ef;\n  color: #ffffff;\n}\n.direct-chat-info .right > .direct-chat-text:after,\n.direct-chat-info .right > .direct-chat-text:before {\n  border-left-color: #00c0ef;\n}\n.direct-chat-success .right > .direct-chat-text {\n  background: #00a65a;\n  border-color: #00a65a;\n  color: #ffffff;\n}\n.direct-chat-success .right > .direct-chat-text:after,\n.direct-chat-success .right > .direct-chat-text:before {\n  border-left-color: #00a65a;\n}\n/*\n * Component: Users List\n * ---------------------\n */\n.users-list > li {\n  width: 25%;\n  float: left;\n  padding: 10px;\n  text-align: center;\n}\n.users-list > li img {\n  border-radius: 50%;\n  max-width: 100%;\n  height: auto;\n}\n.users-list > li > a:hover,\n.users-list > li > a:hover .users-list-name {\n  color: #999;\n}\n.users-list-name,\n.users-list-date {\n  display: block;\n}\n.users-list-name {\n  font-weight: 600;\n  color: #444;\n  overflow: hidden;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n.users-list-date {\n  color: #999;\n  font-size: 12px;\n}\n/*\n * Component: Carousel\n * -------------------\n */\n.carousel-control.left,\n.carousel-control.right {\n  background-image: none;\n}\n.carousel-control > .fa {\n  font-size: 40px;\n  position: absolute;\n  top: 50%;\n  z-index: 5;\n  display: inline-block;\n  margin-top: -20px;\n}\n/*\n * Component: modal\n * ----------------\n */\n.modal {\n  background: rgba(0, 0, 0, 0.3);\n}\n.modal-content {\n  border-radius: 0;\n  -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);\n  box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);\n  border: 0;\n}\n@media (min-width: 768px) {\n  .modal-content {\n    -webkit-box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);\n    box-shadow: 0 2px 3px rgba(0, 0, 0, 0.125);\n  }\n}\n.modal-header {\n  border-bottom-color: #f4f4f4;\n}\n.modal-footer {\n  border-top-color: #f4f4f4;\n}\n.modal-primary .modal-header,\n.modal-primary .modal-footer {\n  border-color: #307095;\n}\n.modal-warning .modal-header,\n.modal-warning .modal-footer {\n  border-color: #c87f0a;\n}\n.modal-info .modal-header,\n.modal-info .modal-footer {\n  border-color: #0097bc;\n}\n.modal-success .modal-header,\n.modal-success .modal-footer {\n  border-color: #00733e;\n}\n.modal-danger .modal-header,\n.modal-danger .modal-footer {\n  border-color: #c23321;\n}\n/*\n * Component: Social Widgets\n * -------------------------\n */\n.box-widget {\n  border: none;\n  position: relative;\n}\n.widget-user .widget-user-header {\n  padding: 20px;\n  height: 120px;\n  border-top-right-radius: 3px;\n  border-top-left-radius: 3px;\n}\n.widget-user .widget-user-username {\n  margin-top: 0;\n  margin-bottom: 5px;\n  font-size: 25px;\n  font-weight: 300;\n  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);\n}\n.widget-user .widget-user-desc {\n  margin-top: 0;\n}\n.widget-user .widget-user-image {\n  position: absolute;\n  top: 65px;\n  left: 50%;\n  margin-left: -45px;\n}\n.widget-user .widget-user-image > img {\n  width: 90px;\n  height: auto;\n  border: 3px solid #fff;\n}\n.widget-user .box-footer {\n  padding-top: 30px;\n}\n.widget-user-2 .widget-user-header {\n  padding: 20px;\n  border-top-right-radius: 3px;\n  border-top-left-radius: 3px;\n}\n.widget-user-2 .widget-user-username {\n  margin-top: 5px;\n  margin-bottom: 5px;\n  font-size: 25px;\n  font-weight: 300;\n}\n.widget-user-2 .widget-user-desc {\n  margin-top: 0;\n}\n.widget-user-2 .widget-user-username,\n.widget-user-2 .widget-user-desc {\n  margin-left: 75px;\n}\n.widget-user-2 .widget-user-image > img {\n  width: 65px;\n  height: auto;\n  float: left;\n}\n/*\n * Page: Mailbox\n * -------------\n */\n.mailbox-messages > .table {\n  margin: 0;\n}\n.mailbox-controls {\n  padding: 5px;\n}\n.mailbox-controls.with-border {\n  border-bottom: 1px solid #f4f4f4;\n}\n.mailbox-read-info {\n  border-bottom: 1px solid #f4f4f4;\n  padding: 10px;\n}\n.mailbox-read-info h3 {\n  font-size: 20px;\n  margin: 0;\n}\n.mailbox-read-info h5 {\n  margin: 0;\n  padding: 5px 0 0 0;\n}\n.mailbox-read-time {\n  color: #999;\n  font-size: 13px;\n}\n.mailbox-read-message {\n  padding: 10px;\n}\n.mailbox-attachments li {\n  float: left;\n  width: 200px;\n  border: 1px solid #eee;\n  margin-bottom: 10px;\n  margin-right: 10px;\n}\n.mailbox-attachment-name {\n  font-weight: bold;\n  color: #666;\n}\n.mailbox-attachment-icon,\n.mailbox-attachment-info,\n.mailbox-attachment-size {\n  display: block;\n}\n.mailbox-attachment-info {\n  padding: 10px;\n  background: #f4f4f4;\n}\n.mailbox-attachment-size {\n  color: #999;\n  font-size: 12px;\n}\n.mailbox-attachment-icon {\n  text-align: center;\n  font-size: 65px;\n  color: #666;\n  padding: 20px 10px;\n}\n.mailbox-attachment-icon.has-img {\n  padding: 0;\n}\n.mailbox-attachment-icon.has-img > img {\n  max-width: 100%;\n  height: auto;\n}\n/*\n * Page: Lock Screen\n * -----------------\n */\n/* ADD THIS CLASS TO THE <BODY> TAG */\n.lockscreen {\n  background: #d2d6de;\n}\n.lockscreen-logo {\n  font-size: 35px;\n  text-align: center;\n  margin-bottom: 25px;\n  font-weight: 300;\n}\n.lockscreen-logo a {\n  color: #444;\n}\n.lockscreen-wrapper {\n  max-width: 400px;\n  margin: 0 auto;\n  margin-top: 10%;\n}\n/* User name [optional] */\n.lockscreen .lockscreen-name {\n  text-align: center;\n  font-weight: 600;\n}\n/* Will contain the image and the sign in form */\n.lockscreen-item {\n  border-radius: 4px;\n  padding: 0;\n  background: #fff;\n  position: relative;\n  margin: 10px auto 30px auto;\n  width: 290px;\n}\n/* User image */\n.lockscreen-image {\n  border-radius: 50%;\n  position: absolute;\n  left: -10px;\n  top: -25px;\n  background: #fff;\n  padding: 5px;\n  z-index: 10;\n}\n.lockscreen-image > img {\n  border-radius: 50%;\n  width: 70px;\n  height: 70px;\n}\n/* Contains the password input and the login button */\n.lockscreen-credentials {\n  margin-left: 70px;\n}\n.lockscreen-credentials .form-control {\n  border: 0;\n}\n.lockscreen-credentials .btn {\n  background-color: #fff;\n  border: 0;\n  padding: 0 10px;\n}\n.lockscreen-footer {\n  margin-top: 10px;\n}\n/*\n * Page: Login & Register\n * ----------------------\n */\n.login-logo,\n.register-logo {\n  font-size: 35px;\n  text-align: center;\n  margin-bottom: 25px;\n  font-weight: 300;\n}\n.login-logo a,\n.register-logo a {\n  color: #444;\n}\n.login-page,\n.register-page {\n  background: #d2d6de;\n}\n.login-box,\n.register-box {\n  width: 360px;\n  margin: 7% auto;\n}\n@media (max-width: 768px) {\n  .login-box,\n  .register-box {\n    width: 90%;\n    margin-top: 20px;\n  }\n}\n.login-box-body,\n.register-box-body {\n  background: #fff;\n  padding: 20px;\n  border-top: 0;\n  color: #666;\n}\n.login-box-body .form-control-feedback,\n.register-box-body .form-control-feedback {\n  color: #777;\n}\n.login-box-msg,\n.register-box-msg {\n  margin: 0;\n  text-align: center;\n  padding: 0 20px 20px 20px;\n}\n.social-auth-links {\n  margin: 10px 0;\n}\n/*\n * Page: 400 and 500 error pages\n * ------------------------------\n */\n.error-page {\n  width: 600px;\n  margin: 20px auto 0 auto;\n}\n@media (max-width: 991px) {\n  .error-page {\n    width: 100%;\n  }\n}\n.error-page > .headline {\n  float: left;\n  font-size: 100px;\n  font-weight: 300;\n}\n@media (max-width: 991px) {\n  .error-page > .headline {\n    float: none;\n    text-align: center;\n  }\n}\n.error-page > .error-content {\n  margin-left: 190px;\n  display: block;\n}\n@media (max-width: 991px) {\n  .error-page > .error-content {\n    margin-left: 0;\n  }\n}\n.error-page > .error-content > h3 {\n  font-weight: 300;\n  font-size: 25px;\n}\n@media (max-width: 991px) {\n  .error-page > .error-content > h3 {\n    text-align: center;\n  }\n}\n/*\n * Page: Invoice\n * -------------\n */\n.invoice {\n  position: relative;\n  background: #fff;\n  border: 1px solid #f4f4f4;\n  padding: 20px;\n  margin: 10px 25px;\n}\n.invoice-title {\n  margin-top: 0;\n}\n/*\n * Page: Profile\n * -------------\n */\n.profile-user-img {\n  margin: 0 auto;\n  width: 100px;\n  padding: 3px;\n  border: 3px solid #d2d6de;\n}\n.profile-username {\n  font-size: 21px;\n  margin-top: 5px;\n}\n.post {\n  border-bottom: 1px solid #d2d6de;\n  margin-bottom: 15px;\n  padding-bottom: 15px;\n  color: #666;\n}\n.post:last-of-type {\n  border-bottom: 0;\n  margin-bottom: 0;\n  padding-bottom: 0;\n}\n.post .user-block {\n  margin-bottom: 15px;\n}\n/*\n * Social Buttons for Bootstrap\n *\n * Copyright 2013-2015 Panayiotis Lipiridis\n * Licensed under the MIT License\n *\n * https://github.com/lipis/bootstrap-social\n */\n.btn-social {\n  position: relative;\n  padding-left: 44px;\n  text-align: left;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n.btn-social > :first-child {\n  position: absolute;\n  left: 0;\n  top: 0;\n  bottom: 0;\n  width: 32px;\n  line-height: 34px;\n  font-size: 1.6em;\n  text-align: center;\n  border-right: 1px solid rgba(0, 0, 0, 0.2);\n}\n.btn-social.btn-lg {\n  padding-left: 61px;\n}\n.btn-social.btn-lg > :first-child {\n  line-height: 45px;\n  width: 45px;\n  font-size: 1.8em;\n}\n.btn-social.btn-sm {\n  padding-left: 38px;\n}\n.btn-social.btn-sm > :first-child {\n  line-height: 28px;\n  width: 28px;\n  font-size: 1.4em;\n}\n.btn-social.btn-xs {\n  padding-left: 30px;\n}\n.btn-social.btn-xs > :first-child {\n  line-height: 20px;\n  width: 20px;\n  font-size: 1.2em;\n}\n.btn-social-icon {\n  position: relative;\n  padding-left: 44px;\n  text-align: left;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  height: 34px;\n  width: 34px;\n  padding: 0;\n}\n.btn-social-icon > :first-child {\n  position: absolute;\n  left: 0;\n  top: 0;\n  bottom: 0;\n  width: 32px;\n  line-height: 34px;\n  font-size: 1.6em;\n  text-align: center;\n  border-right: 1px solid rgba(0, 0, 0, 0.2);\n}\n.btn-social-icon.btn-lg {\n  padding-left: 61px;\n}\n.btn-social-icon.btn-lg > :first-child {\n  line-height: 45px;\n  width: 45px;\n  font-size: 1.8em;\n}\n.btn-social-icon.btn-sm {\n  padding-left: 38px;\n}\n.btn-social-icon.btn-sm > :first-child {\n  line-height: 28px;\n  width: 28px;\n  font-size: 1.4em;\n}\n.btn-social-icon.btn-xs {\n  padding-left: 30px;\n}\n.btn-social-icon.btn-xs > :first-child {\n  line-height: 20px;\n  width: 20px;\n  font-size: 1.2em;\n}\n.btn-social-icon > :first-child {\n  border: none;\n  text-align: center;\n  width: 100%;\n}\n.btn-social-icon.btn-lg {\n  height: 45px;\n  width: 45px;\n  padding-left: 0;\n  padding-right: 0;\n}\n.btn-social-icon.btn-sm {\n  height: 30px;\n  width: 30px;\n  padding-left: 0;\n  padding-right: 0;\n}\n.btn-social-icon.btn-xs {\n  height: 22px;\n  width: 22px;\n  padding-left: 0;\n  padding-right: 0;\n}\n.btn-adn {\n  color: #ffffff;\n  background-color: #d87a68;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-adn:focus,\n.btn-adn.focus {\n  color: #ffffff;\n  background-color: #ce563f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-adn:hover {\n  color: #ffffff;\n  background-color: #ce563f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-adn:active,\n.btn-adn.active,\n.open > .dropdown-toggle.btn-adn {\n  color: #ffffff;\n  background-color: #ce563f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-adn:active,\n.btn-adn.active,\n.open > .dropdown-toggle.btn-adn {\n  background-image: none;\n}\n.btn-adn .badge {\n  color: #d87a68;\n  background-color: #ffffff;\n}\n.btn-bitbucket {\n  color: #ffffff;\n  background-color: #205081;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-bitbucket:focus,\n.btn-bitbucket.focus {\n  color: #ffffff;\n  background-color: #163758;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-bitbucket:hover {\n  color: #ffffff;\n  background-color: #163758;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-bitbucket:active,\n.btn-bitbucket.active,\n.open > .dropdown-toggle.btn-bitbucket {\n  color: #ffffff;\n  background-color: #163758;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-bitbucket:active,\n.btn-bitbucket.active,\n.open > .dropdown-toggle.btn-bitbucket {\n  background-image: none;\n}\n.btn-bitbucket .badge {\n  color: #205081;\n  background-color: #ffffff;\n}\n.btn-dropbox {\n  color: #ffffff;\n  background-color: #1087dd;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-dropbox:focus,\n.btn-dropbox.focus {\n  color: #ffffff;\n  background-color: #0d6aad;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-dropbox:hover {\n  color: #ffffff;\n  background-color: #0d6aad;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-dropbox:active,\n.btn-dropbox.active,\n.open > .dropdown-toggle.btn-dropbox {\n  color: #ffffff;\n  background-color: #0d6aad;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-dropbox:active,\n.btn-dropbox.active,\n.open > .dropdown-toggle.btn-dropbox {\n  background-image: none;\n}\n.btn-dropbox .badge {\n  color: #1087dd;\n  background-color: #ffffff;\n}\n.btn-facebook {\n  color: #ffffff;\n  background-color: #3b5998;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-facebook:focus,\n.btn-facebook.focus {\n  color: #ffffff;\n  background-color: #2d4373;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-facebook:hover {\n  color: #ffffff;\n  background-color: #2d4373;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-facebook:active,\n.btn-facebook.active,\n.open > .dropdown-toggle.btn-facebook {\n  color: #ffffff;\n  background-color: #2d4373;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-facebook:active,\n.btn-facebook.active,\n.open > .dropdown-toggle.btn-facebook {\n  background-image: none;\n}\n.btn-facebook .badge {\n  color: #3b5998;\n  background-color: #ffffff;\n}\n.btn-flickr {\n  color: #ffffff;\n  background-color: #ff0084;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-flickr:focus,\n.btn-flickr.focus {\n  color: #ffffff;\n  background-color: #cc006a;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-flickr:hover {\n  color: #ffffff;\n  background-color: #cc006a;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-flickr:active,\n.btn-flickr.active,\n.open > .dropdown-toggle.btn-flickr {\n  color: #ffffff;\n  background-color: #cc006a;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-flickr:active,\n.btn-flickr.active,\n.open > .dropdown-toggle.btn-flickr {\n  background-image: none;\n}\n.btn-flickr .badge {\n  color: #ff0084;\n  background-color: #ffffff;\n}\n.btn-foursquare {\n  color: #ffffff;\n  background-color: #f94877;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-foursquare:focus,\n.btn-foursquare.focus {\n  color: #ffffff;\n  background-color: #f71752;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-foursquare:hover {\n  color: #ffffff;\n  background-color: #f71752;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-foursquare:active,\n.btn-foursquare.active,\n.open > .dropdown-toggle.btn-foursquare {\n  color: #ffffff;\n  background-color: #f71752;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-foursquare:active,\n.btn-foursquare.active,\n.open > .dropdown-toggle.btn-foursquare {\n  background-image: none;\n}\n.btn-foursquare .badge {\n  color: #f94877;\n  background-color: #ffffff;\n}\n.btn-github {\n  color: #ffffff;\n  background-color: #444444;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-github:focus,\n.btn-github.focus {\n  color: #ffffff;\n  background-color: #2b2b2b;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-github:hover {\n  color: #ffffff;\n  background-color: #2b2b2b;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-github:active,\n.btn-github.active,\n.open > .dropdown-toggle.btn-github {\n  color: #ffffff;\n  background-color: #2b2b2b;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-github:active,\n.btn-github.active,\n.open > .dropdown-toggle.btn-github {\n  background-image: none;\n}\n.btn-github .badge {\n  color: #444444;\n  background-color: #ffffff;\n}\n.btn-google {\n  color: #ffffff;\n  background-color: #dd4b39;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-google:focus,\n.btn-google.focus {\n  color: #ffffff;\n  background-color: #c23321;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-google:hover {\n  color: #ffffff;\n  background-color: #c23321;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-google:active,\n.btn-google.active,\n.open > .dropdown-toggle.btn-google {\n  color: #ffffff;\n  background-color: #c23321;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-google:active,\n.btn-google.active,\n.open > .dropdown-toggle.btn-google {\n  background-image: none;\n}\n.btn-google .badge {\n  color: #dd4b39;\n  background-color: #ffffff;\n}\n.btn-instagram {\n  color: #ffffff;\n  background-color: #3f729b;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-instagram:focus,\n.btn-instagram.focus {\n  color: #ffffff;\n  background-color: #305777;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-instagram:hover {\n  color: #ffffff;\n  background-color: #305777;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-instagram:active,\n.btn-instagram.active,\n.open > .dropdown-toggle.btn-instagram {\n  color: #ffffff;\n  background-color: #305777;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-instagram:active,\n.btn-instagram.active,\n.open > .dropdown-toggle.btn-instagram {\n  background-image: none;\n}\n.btn-instagram .badge {\n  color: #3f729b;\n  background-color: #ffffff;\n}\n.btn-linkedin {\n  color: #ffffff;\n  background-color: #007bb6;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-linkedin:focus,\n.btn-linkedin.focus {\n  color: #ffffff;\n  background-color: #005983;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-linkedin:hover {\n  color: #ffffff;\n  background-color: #005983;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-linkedin:active,\n.btn-linkedin.active,\n.open > .dropdown-toggle.btn-linkedin {\n  color: #ffffff;\n  background-color: #005983;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-linkedin:active,\n.btn-linkedin.active,\n.open > .dropdown-toggle.btn-linkedin {\n  background-image: none;\n}\n.btn-linkedin .badge {\n  color: #007bb6;\n  background-color: #ffffff;\n}\n.btn-microsoft {\n  color: #ffffff;\n  background-color: #2672ec;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-microsoft:focus,\n.btn-microsoft.focus {\n  color: #ffffff;\n  background-color: #125acd;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-microsoft:hover {\n  color: #ffffff;\n  background-color: #125acd;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-microsoft:active,\n.btn-microsoft.active,\n.open > .dropdown-toggle.btn-microsoft {\n  color: #ffffff;\n  background-color: #125acd;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-microsoft:active,\n.btn-microsoft.active,\n.open > .dropdown-toggle.btn-microsoft {\n  background-image: none;\n}\n.btn-microsoft .badge {\n  color: #2672ec;\n  background-color: #ffffff;\n}\n.btn-openid {\n  color: #ffffff;\n  background-color: #f7931e;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-openid:focus,\n.btn-openid.focus {\n  color: #ffffff;\n  background-color: #da7908;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-openid:hover {\n  color: #ffffff;\n  background-color: #da7908;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-openid:active,\n.btn-openid.active,\n.open > .dropdown-toggle.btn-openid {\n  color: #ffffff;\n  background-color: #da7908;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-openid:active,\n.btn-openid.active,\n.open > .dropdown-toggle.btn-openid {\n  background-image: none;\n}\n.btn-openid .badge {\n  color: #f7931e;\n  background-color: #ffffff;\n}\n.btn-pinterest {\n  color: #ffffff;\n  background-color: #cb2027;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-pinterest:focus,\n.btn-pinterest.focus {\n  color: #ffffff;\n  background-color: #9f191f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-pinterest:hover {\n  color: #ffffff;\n  background-color: #9f191f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-pinterest:active,\n.btn-pinterest.active,\n.open > .dropdown-toggle.btn-pinterest {\n  color: #ffffff;\n  background-color: #9f191f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-pinterest:active,\n.btn-pinterest.active,\n.open > .dropdown-toggle.btn-pinterest {\n  background-image: none;\n}\n.btn-pinterest .badge {\n  color: #cb2027;\n  background-color: #ffffff;\n}\n.btn-reddit {\n  color: #000000;\n  background-color: #eff7ff;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-reddit:focus,\n.btn-reddit.focus {\n  color: #000000;\n  background-color: #bcddff;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-reddit:hover {\n  color: #000000;\n  background-color: #bcddff;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-reddit:active,\n.btn-reddit.active,\n.open > .dropdown-toggle.btn-reddit {\n  color: #000000;\n  background-color: #bcddff;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-reddit:active,\n.btn-reddit.active,\n.open > .dropdown-toggle.btn-reddit {\n  background-image: none;\n}\n.btn-reddit .badge {\n  color: #eff7ff;\n  background-color: #000000;\n}\n.btn-soundcloud {\n  color: #ffffff;\n  background-color: #ff5500;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-soundcloud:focus,\n.btn-soundcloud.focus {\n  color: #ffffff;\n  background-color: #cc4400;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-soundcloud:hover {\n  color: #ffffff;\n  background-color: #cc4400;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-soundcloud:active,\n.btn-soundcloud.active,\n.open > .dropdown-toggle.btn-soundcloud {\n  color: #ffffff;\n  background-color: #cc4400;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-soundcloud:active,\n.btn-soundcloud.active,\n.open > .dropdown-toggle.btn-soundcloud {\n  background-image: none;\n}\n.btn-soundcloud .badge {\n  color: #ff5500;\n  background-color: #ffffff;\n}\n.btn-tumblr {\n  color: #ffffff;\n  background-color: #2c4762;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-tumblr:focus,\n.btn-tumblr.focus {\n  color: #ffffff;\n  background-color: #1c2d3f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-tumblr:hover {\n  color: #ffffff;\n  background-color: #1c2d3f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-tumblr:active,\n.btn-tumblr.active,\n.open > .dropdown-toggle.btn-tumblr {\n  color: #ffffff;\n  background-color: #1c2d3f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-tumblr:active,\n.btn-tumblr.active,\n.open > .dropdown-toggle.btn-tumblr {\n  background-image: none;\n}\n.btn-tumblr .badge {\n  color: #2c4762;\n  background-color: #ffffff;\n}\n.btn-twitter {\n  color: #ffffff;\n  background-color: #55acee;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-twitter:focus,\n.btn-twitter.focus {\n  color: #ffffff;\n  background-color: #2795e9;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-twitter:hover {\n  color: #ffffff;\n  background-color: #2795e9;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-twitter:active,\n.btn-twitter.active,\n.open > .dropdown-toggle.btn-twitter {\n  color: #ffffff;\n  background-color: #2795e9;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-twitter:active,\n.btn-twitter.active,\n.open > .dropdown-toggle.btn-twitter {\n  background-image: none;\n}\n.btn-twitter .badge {\n  color: #55acee;\n  background-color: #ffffff;\n}\n.btn-vimeo {\n  color: #ffffff;\n  background-color: #1ab7ea;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vimeo:focus,\n.btn-vimeo.focus {\n  color: #ffffff;\n  background-color: #1295bf;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vimeo:hover {\n  color: #ffffff;\n  background-color: #1295bf;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vimeo:active,\n.btn-vimeo.active,\n.open > .dropdown-toggle.btn-vimeo {\n  color: #ffffff;\n  background-color: #1295bf;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vimeo:active,\n.btn-vimeo.active,\n.open > .dropdown-toggle.btn-vimeo {\n  background-image: none;\n}\n.btn-vimeo .badge {\n  color: #1ab7ea;\n  background-color: #ffffff;\n}\n.btn-vk {\n  color: #ffffff;\n  background-color: #587ea3;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vk:focus,\n.btn-vk.focus {\n  color: #ffffff;\n  background-color: #466482;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vk:hover {\n  color: #ffffff;\n  background-color: #466482;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vk:active,\n.btn-vk.active,\n.open > .dropdown-toggle.btn-vk {\n  color: #ffffff;\n  background-color: #466482;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-vk:active,\n.btn-vk.active,\n.open > .dropdown-toggle.btn-vk {\n  background-image: none;\n}\n.btn-vk .badge {\n  color: #587ea3;\n  background-color: #ffffff;\n}\n.btn-yahoo {\n  color: #ffffff;\n  background-color: #720e9e;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-yahoo:focus,\n.btn-yahoo.focus {\n  color: #ffffff;\n  background-color: #500a6f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-yahoo:hover {\n  color: #ffffff;\n  background-color: #500a6f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-yahoo:active,\n.btn-yahoo.active,\n.open > .dropdown-toggle.btn-yahoo {\n  color: #ffffff;\n  background-color: #500a6f;\n  border-color: rgba(0, 0, 0, 0.2);\n}\n.btn-yahoo:active,\n.btn-yahoo.active,\n.open > .dropdown-toggle.btn-yahoo {\n  background-image: none;\n}\n.btn-yahoo .badge {\n  color: #720e9e;\n  background-color: #ffffff;\n}\n/*\n * Plugin: Full Calendar\n * ---------------------\n */\n.fc-button {\n  background: #f4f4f4;\n  background-image: none;\n  color: #444;\n  border-color: #ddd;\n  border-bottom-color: #ddd;\n}\n.fc-button:hover,\n.fc-button:active,\n.fc-button.hover {\n  background-color: #e9e9e9;\n}\n.fc-header-title h2 {\n  font-size: 15px;\n  line-height: 1.6em;\n  color: #666;\n  margin-left: 10px;\n}\n.fc-header-right {\n  padding-right: 10px;\n}\n.fc-header-left {\n  padding-left: 10px;\n}\n.fc-widget-header {\n  background: #fafafa;\n}\n.fc-grid {\n  width: 100%;\n  border: 0;\n}\n.fc-widget-header:first-of-type,\n.fc-widget-content:first-of-type {\n  border-left: 0;\n  border-right: 0;\n}\n.fc-widget-header:last-of-type,\n.fc-widget-content:last-of-type {\n  border-right: 0;\n}\n.fc-toolbar {\n  padding: 10px;\n  margin: 0;\n}\n.fc-day-number {\n  font-size: 20px;\n  font-weight: 300;\n  padding-right: 10px;\n}\n.fc-color-picker {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.fc-color-picker > li {\n  float: left;\n  font-size: 30px;\n  margin-right: 5px;\n  line-height: 30px;\n}\n.fc-color-picker > li .fa {\n  -webkit-transition: -webkit-transform linear 0.3s;\n  -moz-transition: -moz-transform linear 0.3s;\n  -o-transition: -o-transform linear 0.3s;\n  transition: transform linear 0.3s;\n}\n.fc-color-picker > li .fa:hover {\n  -webkit-transform: rotate(30deg);\n  -ms-transform: rotate(30deg);\n  -o-transform: rotate(30deg);\n  transform: rotate(30deg);\n}\n#add-new-event {\n  -webkit-transition: all linear 0.3s;\n  -o-transition: all linear 0.3s;\n  transition: all linear 0.3s;\n}\n.external-event {\n  padding: 5px 10px;\n  font-weight: bold;\n  margin-bottom: 4px;\n  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);\n  border-radius: 3px;\n  cursor: move;\n}\n.external-event:hover {\n  box-shadow: inset 0 0 90px rgba(0, 0, 0, 0.2);\n}\n/*\n * Plugin: Select2\n * ---------------\n */\n.select2-container--default.select2-container--focus,\n.select2-selection.select2-container--focus,\n.select2-container--default:focus,\n.select2-selection:focus,\n.select2-container--default:active,\n.select2-selection:active {\n  outline: none;\n}\n.select2-container--default .select2-selection--single,\n.select2-selection .select2-selection--single {\n  border: 1px solid #d2d6de;\n  border-radius: 0;\n  padding: 6px 12px;\n  height: 34px;\n}\n.select2-container--default.select2-container--open {\n  border-color: #3c8dbc;\n}\n.select2-dropdown {\n  border: 1px solid #d2d6de;\n  border-radius: 0;\n}\n.select2-container--default .select2-results__option--highlighted[aria-selected] {\n  background-color: #3c8dbc;\n  color: white;\n}\n.select2-results__option {\n  padding: 6px 12px;\n  user-select: none;\n  -webkit-user-select: none;\n}\n.select2-container .select2-selection--single .select2-selection__rendered {\n  padding-left: 0;\n  padding-right: 0;\n  height: auto;\n  margin-top: -4px;\n}\n.select2-container[dir=\"rtl\"] .select2-selection--single .select2-selection__rendered {\n  padding-right: 6px;\n  padding-left: 20px;\n}\n.select2-container--default .select2-selection--single .select2-selection__arrow {\n  height: 28px;\n  right: 3px;\n}\n.select2-container--default .select2-selection--single .select2-selection__arrow b {\n  margin-top: 0;\n}\n.select2-dropdown .select2-search__field,\n.select2-search--inline .select2-search__field {\n  border: 1px solid #d2d6de;\n}\n.select2-dropdown .select2-search__field:focus,\n.select2-search--inline .select2-search__field:focus {\n  outline: none;\n  border: 1px solid #3c8dbc;\n}\n.select2-container--default .select2-results__option[aria-disabled=true] {\n  color: #999;\n}\n.select2-container--default .select2-results__option[aria-selected=true] {\n  background-color: #ddd;\n}\n.select2-container--default .select2-results__option[aria-selected=true],\n.select2-container--default .select2-results__option[aria-selected=true]:hover {\n  color: #444;\n}\n.select2-container--default .select2-selection--multiple {\n  border: 1px solid #d2d6de;\n  border-radius: 0;\n}\n.select2-container--default .select2-selection--multiple:focus {\n  border-color: #3c8dbc;\n}\n.select2-container--default.select2-container--focus .select2-selection--multiple {\n  border-color: #d2d6de;\n}\n.select2-container--default .select2-selection--multiple .select2-selection__choice {\n  background-color: #3c8dbc;\n  border-color: #367fa9;\n  padding: 1px 10px;\n  color: #fff;\n}\n.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {\n  margin-right: 5px;\n  color: rgba(255, 255, 255, 0.7);\n}\n.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover {\n  color: #fff;\n}\n.select2-container .select2-selection--single .select2-selection__rendered {\n  padding-right: 10px;\n}\n/*\n * General: Miscellaneous\n * ----------------------\n */\n.pad {\n  padding: 10px;\n}\n.margin {\n  margin: 10px;\n}\n.margin-bottom {\n  margin-bottom: 20px;\n}\n.margin-bottom-none {\n  margin-bottom: 0;\n}\n.margin-r-5 {\n  margin-right: 5px;\n}\n.inline {\n  display: inline;\n}\n.description-block {\n  display: block;\n  margin: 10px 0;\n  text-align: center;\n}\n.description-block.margin-bottom {\n  margin-bottom: 25px;\n}\n.description-block > .description-header {\n  margin: 0;\n  padding: 0;\n  font-weight: 600;\n  font-size: 16px;\n}\n.description-block > .description-text {\n  text-transform: uppercase;\n}\n.bg-red,\n.bg-yellow,\n.bg-aqua,\n.bg-blue,\n.bg-light-blue,\n.bg-green,\n.bg-navy,\n.bg-teal,\n.bg-olive,\n.bg-lime,\n.bg-orange,\n.bg-fuchsia,\n.bg-purple,\n.bg-maroon,\n.bg-black,\n.bg-red-active,\n.bg-yellow-active,\n.bg-aqua-active,\n.bg-blue-active,\n.bg-light-blue-active,\n.bg-green-active,\n.bg-navy-active,\n.bg-teal-active,\n.bg-olive-active,\n.bg-lime-active,\n.bg-orange-active,\n.bg-fuchsia-active,\n.bg-purple-active,\n.bg-maroon-active,\n.bg-black-active,\n.callout.callout-danger,\n.callout.callout-warning,\n.callout.callout-info,\n.callout.callout-success,\n.alert-success,\n.alert-danger,\n.alert-error,\n.alert-warning,\n.alert-info,\n.label-danger,\n.label-info,\n.label-warning,\n.label-primary,\n.label-success,\n.modal-primary .modal-body,\n.modal-primary .modal-header,\n.modal-primary .modal-footer,\n.modal-warning .modal-body,\n.modal-warning .modal-header,\n.modal-warning .modal-footer,\n.modal-info .modal-body,\n.modal-info .modal-header,\n.modal-info .modal-footer,\n.modal-success .modal-body,\n.modal-success .modal-header,\n.modal-success .modal-footer,\n.modal-danger .modal-body,\n.modal-danger .modal-header,\n.modal-danger .modal-footer {\n  color: #fff !important;\n}\n.bg-gray {\n  color: #000;\n  background-color: #d2d6de !important;\n}\n.bg-gray-light {\n  background-color: #f7f7f7;\n}\n.bg-black {\n  background-color: #111111 !important;\n}\n.bg-red,\n.callout.callout-danger,\n.alert-danger,\n.alert-error,\n.label-danger,\n.modal-danger .modal-body {\n  background-color: #dd4b39 !important;\n}\n.bg-yellow,\n.callout.callout-warning,\n.alert-warning,\n.label-warning,\n.modal-warning .modal-body {\n  background-color: #f39c12 !important;\n}\n.bg-aqua,\n.callout.callout-info,\n.alert-info,\n.label-info,\n.modal-info .modal-body {\n  background-color: #00c0ef !important;\n}\n.bg-blue {\n  background-color: #0073b7 !important;\n}\n.bg-light-blue,\n.label-primary,\n.modal-primary .modal-body {\n  background-color: #3c8dbc !important;\n}\n.bg-green,\n.callout.callout-success,\n.alert-success,\n.label-success,\n.modal-success .modal-body {\n  background-color: #00a65a !important;\n}\n.bg-navy {\n  background-color: #001f3f !important;\n}\n.bg-teal {\n  background-color: #39cccc !important;\n}\n.bg-olive {\n  background-color: #3d9970 !important;\n}\n.bg-lime {\n  background-color: #01ff70 !important;\n}\n.bg-orange {\n  background-color: #ff851b !important;\n}\n.bg-fuchsia {\n  background-color: #f012be !important;\n}\n.bg-purple {\n  background-color: #605ca8 !important;\n}\n.bg-maroon {\n  background-color: #d81b60 !important;\n}\n.bg-gray-active {\n  color: #000;\n  background-color: #b5bbc8 !important;\n}\n.bg-black-active {\n  background-color: #000000 !important;\n}\n.bg-red-active,\n.modal-danger .modal-header,\n.modal-danger .modal-footer {\n  background-color: #d33724 !important;\n}\n.bg-yellow-active,\n.modal-warning .modal-header,\n.modal-warning .modal-footer {\n  background-color: #db8b0b !important;\n}\n.bg-aqua-active,\n.modal-info .modal-header,\n.modal-info .modal-footer {\n  background-color: #00a7d0 !important;\n}\n.bg-blue-active {\n  background-color: #005384 !important;\n}\n.bg-light-blue-active,\n.modal-primary .modal-header,\n.modal-primary .modal-footer {\n  background-color: #357ca5 !important;\n}\n.bg-green-active,\n.modal-success .modal-header,\n.modal-success .modal-footer {\n  background-color: #008d4c !important;\n}\n.bg-navy-active {\n  background-color: #001a35 !important;\n}\n.bg-teal-active {\n  background-color: #30bbbb !important;\n}\n.bg-olive-active {\n  background-color: #368763 !important;\n}\n.bg-lime-active {\n  background-color: #00e765 !important;\n}\n.bg-orange-active {\n  background-color: #ff7701 !important;\n}\n.bg-fuchsia-active {\n  background-color: #db0ead !important;\n}\n.bg-purple-active {\n  background-color: #555299 !important;\n}\n.bg-maroon-active {\n  background-color: #ca195a !important;\n}\n[class^=\"bg-\"].disabled {\n  opacity: 0.65;\n  filter: alpha(opacity=65);\n}\n.text-red {\n  color: #dd4b39 !important;\n}\n.text-yellow {\n  color: #f39c12 !important;\n}\n.text-aqua {\n  color: #00c0ef !important;\n}\n.text-blue {\n  color: #0073b7 !important;\n}\n.text-black {\n  color: #111111 !important;\n}\n.text-light-blue {\n  color: #3c8dbc !important;\n}\n.text-green {\n  color: #00a65a !important;\n}\n.text-gray {\n  color: #d2d6de !important;\n}\n.text-navy {\n  color: #001f3f !important;\n}\n.text-teal {\n  color: #39cccc !important;\n}\n.text-olive {\n  color: #3d9970 !important;\n}\n.text-lime {\n  color: #01ff70 !important;\n}\n.text-orange {\n  color: #ff851b !important;\n}\n.text-fuchsia {\n  color: #f012be !important;\n}\n.text-purple {\n  color: #605ca8 !important;\n}\n.text-maroon {\n  color: #d81b60 !important;\n}\n.link-muted {\n  color: #7a869d;\n}\n.link-muted:hover,\n.link-muted:focus {\n  color: #606c84;\n}\n.link-black {\n  color: #666;\n}\n.link-black:hover,\n.link-black:focus {\n  color: #999;\n}\n.hide {\n  display: none !important;\n}\n.no-border {\n  border: 0 !important;\n}\n.no-padding {\n  padding: 0 !important;\n}\n.no-margin {\n  margin: 0 !important;\n}\n.no-shadow {\n  box-shadow: none !important;\n}\n.list-unstyled,\n.chart-legend,\n.contacts-list,\n.users-list,\n.mailbox-attachments {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n.list-group-unbordered > .list-group-item {\n  border-left: 0;\n  border-right: 0;\n  border-radius: 0;\n  padding-left: 0;\n  padding-right: 0;\n}\n.flat {\n  border-radius: 0 !important;\n}\n.text-bold,\n.text-bold.table td,\n.text-bold.table th {\n  font-weight: 700;\n}\n.text-sm {\n  font-size: 12px;\n}\n.jqstooltip {\n  padding: 5px !important;\n  width: auto !important;\n  height: auto !important;\n}\n.bg-teal-gradient {\n  background: #39cccc !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;\n  background: -ms-linear-gradient(bottom, #39cccc, #7adddd) !important;\n  background: -moz-linear-gradient(center bottom, #39cccc 0%, #7adddd 100%) !important;\n  background: -o-linear-gradient(#7adddd, #39cccc) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;\n  color: #fff;\n}\n.bg-light-blue-gradient {\n  background: #3c8dbc !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #3c8dbc), color-stop(1, #67a8ce)) !important;\n  background: -ms-linear-gradient(bottom, #3c8dbc, #67a8ce) !important;\n  background: -moz-linear-gradient(center bottom, #3c8dbc 0%, #67a8ce 100%) !important;\n  background: -o-linear-gradient(#67a8ce, #3c8dbc) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0) !important;\n  color: #fff;\n}\n.bg-blue-gradient {\n  background: #0073b7 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #0073b7), color-stop(1, #0089db)) !important;\n  background: -ms-linear-gradient(bottom, #0073b7, #0089db) !important;\n  background: -moz-linear-gradient(center bottom, #0073b7 0%, #0089db 100%) !important;\n  background: -o-linear-gradient(#0089db, #0073b7) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0) !important;\n  color: #fff;\n}\n.bg-aqua-gradient {\n  background: #00c0ef !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;\n  background: -ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;\n  background: -moz-linear-gradient(center bottom, #00c0ef 0%, #14d1ff 100%) !important;\n  background: -o-linear-gradient(#14d1ff, #00c0ef) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;\n  color: #fff;\n}\n.bg-yellow-gradient {\n  background: #f39c12 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;\n  background: -ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;\n  background: -moz-linear-gradient(center bottom, #f39c12 0%, #f7bc60 100%) !important;\n  background: -o-linear-gradient(#f7bc60, #f39c12) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;\n  color: #fff;\n}\n.bg-purple-gradient {\n  background: #605ca8 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;\n  background: -ms-linear-gradient(bottom, #605ca8, #9491c4) !important;\n  background: -moz-linear-gradient(center bottom, #605ca8 0%, #9491c4 100%) !important;\n  background: -o-linear-gradient(#9491c4, #605ca8) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;\n  color: #fff;\n}\n.bg-green-gradient {\n  background: #00a65a !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;\n  background: -ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;\n  background: -moz-linear-gradient(center bottom, #00a65a 0%, #00ca6d 100%) !important;\n  background: -o-linear-gradient(#00ca6d, #00a65a) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;\n  color: #fff;\n}\n.bg-red-gradient {\n  background: #dd4b39 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;\n  background: -ms-linear-gradient(bottom, #dd4b39, #e47365) !important;\n  background: -moz-linear-gradient(center bottom, #dd4b39 0%, #e47365 100%) !important;\n  background: -o-linear-gradient(#e47365, #dd4b39) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;\n  color: #fff;\n}\n.bg-black-gradient {\n  background: #111111 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #111111), color-stop(1, #2b2b2b)) !important;\n  background: -ms-linear-gradient(bottom, #111111, #2b2b2b) !important;\n  background: -moz-linear-gradient(center bottom, #111111 0%, #2b2b2b 100%) !important;\n  background: -o-linear-gradient(#2b2b2b, #111111) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;\n  color: #fff;\n}\n.bg-maroon-gradient {\n  background: #d81b60 !important;\n  background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;\n  background: -ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;\n  background: -moz-linear-gradient(center bottom, #d81b60 0%, #e73f7c 100%) !important;\n  background: -o-linear-gradient(#e73f7c, #d81b60) !important;\n  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;\n  color: #fff;\n}\n.description-block .description-icon {\n  font-size: 16px;\n}\n.no-pad-top {\n  padding-top: 0;\n}\n.position-static {\n  position: static !important;\n}\n.list-header {\n  font-size: 15px;\n  padding: 10px 4px;\n  font-weight: bold;\n  color: #666;\n}\n.list-seperator {\n  height: 1px;\n  background: #f4f4f4;\n  margin: 15px 0 9px 0;\n}\n.list-link > a {\n  padding: 4px;\n  color: #777;\n}\n.list-link > a:hover {\n  color: #222;\n}\n.font-light {\n  font-weight: 300;\n}\n.user-block:before,\n.user-block:after {\n  content: \" \";\n  display: table;\n}\n.user-block:after {\n  clear: both;\n}\n.user-block img {\n  width: 40px;\n  height: 40px;\n  float: left;\n}\n.user-block .username,\n.user-block .description,\n.user-block .comment {\n  display: block;\n  margin-left: 50px;\n}\n.user-block .username {\n  font-size: 16px;\n  font-weight: 600;\n}\n.user-block .description {\n  color: #999;\n  font-size: 13px;\n}\n.user-block.user-block-sm .username,\n.user-block.user-block-sm .description,\n.user-block.user-block-sm .comment {\n  margin-left: 40px;\n}\n.user-block.user-block-sm .username {\n  font-size: 14px;\n}\n.img-sm,\n.img-md,\n.img-lg,\n.box-comments .box-comment img,\n.user-block.user-block-sm img {\n  float: left;\n}\n.img-sm,\n.box-comments .box-comment img,\n.user-block.user-block-sm img {\n  width: 30px !important;\n  height: 30px !important;\n}\n.img-sm + .img-push {\n  margin-left: 40px;\n}\n.img-md {\n  width: 60px;\n  height: 60px;\n}\n.img-md + .img-push {\n  margin-left: 70px;\n}\n.img-lg {\n  width: 100px;\n  height: 100px;\n}\n.img-lg + .img-push {\n  margin-left: 110px;\n}\n.img-bordered {\n  border: 3px solid #d2d6de;\n  padding: 3px;\n}\n.img-bordered-sm {\n  border: 2px solid #d2d6de;\n  padding: 2px;\n}\n.attachment-block {\n  border: 1px solid #f4f4f4;\n  padding: 5px;\n  margin-bottom: 10px;\n  background: #f7f7f7;\n}\n.attachment-block .attachment-img {\n  max-width: 100px;\n  max-height: 100px;\n  height: auto;\n  float: left;\n}\n.attachment-block .attachment-pushed {\n  margin-left: 110px;\n}\n.attachment-block .attachment-heading {\n  margin: 0;\n}\n.attachment-block .attachment-text {\n  color: #555;\n}\n.connectedSortable {\n  min-height: 100px;\n}\n.ui-helper-hidden-accessible {\n  border: 0;\n  clip: rect(0 0 0 0);\n  height: 1px;\n  margin: -1px;\n  overflow: hidden;\n  padding: 0;\n  position: absolute;\n  width: 1px;\n}\n.sort-highlight {\n  background: #f4f4f4;\n  border: 1px dashed #ddd;\n  margin-bottom: 10px;\n}\n.full-opacity-hover {\n  opacity: 0.65;\n  filter: alpha(opacity=65);\n}\n.full-opacity-hover:hover {\n  opacity: 1;\n  filter: alpha(opacity=100);\n}\n.chart {\n  position: relative;\n  overflow: hidden;\n  width: 100%;\n}\n.chart svg,\n.chart canvas {\n  width: 100% !important;\n}\n/*\n * Misc: print\n * -----------\n */\n@media print {\n  .no-print,\n  .main-sidebar,\n  .left-side,\n  .main-header,\n  .content-header {\n    display: none !important;\n  }\n  .content-wrapper,\n  .right-side,\n  .main-footer {\n    margin-left: 0 !important;\n    min-height: 0 !important;\n    -webkit-transform: translate(0, 0) !important;\n    -ms-transform: translate(0, 0) !important;\n    -o-transform: translate(0, 0) !important;\n    transform: translate(0, 0) !important;\n  }\n  .fixed .content-wrapper,\n  .fixed .right-side {\n    padding-top: 0 !important;\n  }\n  .invoice {\n    width: 100%;\n    border: 0;\n    margin: 0;\n    padding: 0;\n  }\n  .invoice-col {\n    float: left;\n    width: 33.3333333%;\n  }\n  .table-responsive {\n    overflow: auto;\n  }\n  .table-responsive > .table tr th,\n  .table-responsive > .table tr td {\n    white-space: normal !important;\n  }\n}\n"
  },
  {
    "path": "web/static/dist/css/filebox.css",
    "content": ".file-box {\n  float: left;\n  width: 220px;\n}\n.file {\n  border: 1px solid #e7eaec;\n  padding: 0;\n  background-color: #ffffff;\n  position: relative;\n  margin-bottom: 20px;\n  margin-right: 20px;\n}\n.file .icon {\n  padding: 15px 10px;\n  text-align: center;\n}\n.file .icon i {\n  font-size: 70px;\n  color: #dadada;\n}\n.file .file-name {\n  padding: 10px;\n  background-color: #f8f8f8;\n  border-top: 1px solid #e7eaec;\n}\n.corner {\n  position: absolute;\n  display: inline-block;\n  width: 0;\n  height: 0;\n  line-height: 0;\n  border: 0.6em solid transparent;\n  border-right: 0.6em solid #f1f1f1;\n  border-bottom: 0.6em solid #f1f1f1;\n  right: 0em;\n  bottom: 0em;\n}\n\n\na:hover{\ntext-decoration:none;\n}\n"
  },
  {
    "path": "web/static/dist/css/flotconfig.css",
    "content": "/* FLOT CHART  */\n.flot-chart {\n  display: block;\n  height: 200px;\n}\n.widget .flot-chart.dashboard-chart {\n  display: block;\n  height: 120px;\n  margin-top: 40px;\n}\n.flot-chart.dashboard-chart {\n  display: block;\n  height: 180px;\n  margin-top: 40px;\n}\n.flot-chart-content {\n  width: 100%;\n  height: 100%;\n}\n.flot-chart-pie-content {\n  width: 200px;\n  height: 200px;\n  margin: auto;\n}\n.jqstooltip {\n  position: absolute;\n  display: block;\n  left: 0px;\n  top: 0px;\n  visibility: hidden;\n  background: #2b303a;\n  background-color: rgba(43, 48, 58, 0.8);\n  color: white;\n  text-align: left;\n  white-space: nowrap;\n  z-index: 10000;\n  padding: 5px 5px 5px 5px;\n  min-height: 22px;\n  border-radius: 3px;\n}\n.jqsfield {\n  color: white;\n  text-align: left;\n}\n.h-200 {\n  min-height: 200px;\n}\n.legendLabel {\n  padding-left: 5px;\n}\n.stat-list li:first-child {\n  margin-top: 0;\n}\n.stat-list {\n  list-style: none;\n  padding: 0;\n  margin: 0;\n}\n.stat-percent {\n  float: right;\n}\n.stat-list li {\n  margin-top: 15px;\n  position: relative;\n}\n"
  },
  {
    "path": "web/static/dist/css/modalconfig.css",
    "content": "/* MODAL */\n.modal-content {\n  background-clip: padding-box;\n  background-color: #FFFFFF;\n  border: 1px solid rgba(0, 0, 0, 0);\n  border-radius: 4px;\n  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);\n  outline: 0 none;\n  position: relative;\n}\n.modal-dialog {\n  z-index: 2200;\n}\n.modal-body {\n  padding: 20px 30px 30px 30px;\n}\n.inmodal .modal-body {\n  background: #f8fafb;\n}\n.inmodal .modal-header {\n  padding: 30px 15px;\n  text-align: center;\n}\n.animated.modal.fade .modal-dialog {\n  -webkit-transform: none;\n  -ms-transform: none;\n  -o-transform: none;\n  transform: none;\n}\n.inmodal .modal-title {\n  font-size: 26px;\n}\n.inmodal .modal-icon {\n  font-size: 84px;\n  color: #e2e3e3;\n}\n.modal-footer {\n  margin-top: 0;\n}\n"
  },
  {
    "path": "web/static/dist/css/skins/_all-skins.css",
    "content": "/*\n * Skin: Blue\n * ----------\n */\n.skin-blue .main-header .navbar {\n  background-color: #3c8dbc;\n}\n.skin-blue .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-blue .main-header .navbar .nav > li > a:hover,\n.skin-blue .main-header .navbar .nav > li > a:active,\n.skin-blue .main-header .navbar .nav > li > a:focus,\n.skin-blue .main-header .navbar .nav .open > a,\n.skin-blue .main-header .navbar .nav .open > a:hover,\n.skin-blue .main-header .navbar .nav .open > a:focus,\n.skin-blue .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-blue .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-blue .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-blue .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-blue .main-header .navbar .sidebar-toggle:hover {\n  background-color: #367fa9;\n}\n@media (max-width: 767px) {\n  .skin-blue .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-blue .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-blue .main-header .navbar .dropdown-menu li a:hover {\n    background: #367fa9;\n  }\n}\n.skin-blue .main-header .logo {\n  background-color: #367fa9;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue .main-header .logo:hover {\n  background-color: #357ca5;\n}\n.skin-blue .main-header li.user-header {\n  background-color: #3c8dbc;\n}\n.skin-blue .content-header {\n  background: transparent;\n}\n.skin-blue .wrapper,\n.skin-blue .main-sidebar,\n.skin-blue .left-side {\n  background-color: #222d32;\n}\n.skin-blue .user-panel > .info,\n.skin-blue .user-panel > .info > a {\n  color: #fff;\n}\n.skin-blue .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-blue .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-blue .sidebar-menu > li:hover > a,\n.skin-blue .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #3c8dbc;\n}\n.skin-blue .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-blue .sidebar a {\n  color: #b8c7ce;\n}\n.skin-blue .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-blue .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-blue .treeview-menu > li.active > a,\n.skin-blue .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-blue .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-blue .sidebar-form input[type=\"text\"],\n.skin-blue .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-blue .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-blue .sidebar-form input[type=\"text\"]:focus,\n.skin-blue .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-blue .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-blue .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n.skin-blue.layout-top-nav .main-header > .logo {\n  background-color: #3c8dbc;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue.layout-top-nav .main-header > .logo:hover {\n  background-color: #3b8ab8;\n}\n/*\n * Skin: Blue\n * ----------\n */\n.skin-blue-light .main-header .navbar {\n  background-color: #3c8dbc;\n}\n.skin-blue-light .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-blue-light .main-header .navbar .nav > li > a:hover,\n.skin-blue-light .main-header .navbar .nav > li > a:active,\n.skin-blue-light .main-header .navbar .nav > li > a:focus,\n.skin-blue-light .main-header .navbar .nav .open > a,\n.skin-blue-light .main-header .navbar .nav .open > a:hover,\n.skin-blue-light .main-header .navbar .nav .open > a:focus,\n.skin-blue-light .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-blue-light .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-blue-light .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-blue-light .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-blue-light .main-header .navbar .sidebar-toggle:hover {\n  background-color: #367fa9;\n}\n@media (max-width: 767px) {\n  .skin-blue-light .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-blue-light .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-blue-light .main-header .navbar .dropdown-menu li a:hover {\n    background: #367fa9;\n  }\n}\n.skin-blue-light .main-header .logo {\n  background-color: #3c8dbc;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue-light .main-header .logo:hover {\n  background-color: #3b8ab8;\n}\n.skin-blue-light .main-header li.user-header {\n  background-color: #3c8dbc;\n}\n.skin-blue-light .content-header {\n  background: transparent;\n}\n.skin-blue-light .wrapper,\n.skin-blue-light .main-sidebar,\n.skin-blue-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-blue-light .content-wrapper,\n.skin-blue-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-blue-light .user-panel > .info,\n.skin-blue-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-blue-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-blue-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-blue-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-blue-light .sidebar-menu > li:hover > a,\n.skin-blue-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-blue-light .sidebar-menu > li.active {\n  border-left-color: #3c8dbc;\n}\n.skin-blue-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-blue-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-blue-light .sidebar a {\n  color: #444444;\n}\n.skin-blue-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-blue-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-blue-light .treeview-menu > li.active > a,\n.skin-blue-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-blue-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-blue-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-blue-light .sidebar-form input[type=\"text\"],\n.skin-blue-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-blue-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-blue-light .sidebar-form input[type=\"text\"]:focus,\n.skin-blue-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-blue-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-blue-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-blue-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n.skin-blue-light .main-footer {\n  border-top-color: #d2d6de;\n}\n.skin-blue.layout-top-nav .main-header > .logo {\n  background-color: #3c8dbc;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue.layout-top-nav .main-header > .logo:hover {\n  background-color: #3b8ab8;\n}\n/*\n * Skin: Black\n * -----------\n */\n/* skin-black navbar */\n.skin-black .main-header {\n  -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05);\n}\n.skin-black .main-header .navbar-toggle {\n  color: #333;\n}\n.skin-black .main-header .navbar-brand {\n  color: #333;\n  border-right: 1px solid #eee;\n}\n.skin-black .main-header > .navbar {\n  background-color: #ffffff;\n}\n.skin-black .main-header > .navbar .nav > li > a {\n  color: #333333;\n}\n.skin-black .main-header > .navbar .nav > li > a:hover,\n.skin-black .main-header > .navbar .nav > li > a:active,\n.skin-black .main-header > .navbar .nav > li > a:focus,\n.skin-black .main-header > .navbar .nav .open > a,\n.skin-black .main-header > .navbar .nav .open > a:hover,\n.skin-black .main-header > .navbar .nav .open > a:focus,\n.skin-black .main-header > .navbar .nav > .active > a {\n  background: #ffffff;\n  color: #999999;\n}\n.skin-black .main-header > .navbar .sidebar-toggle {\n  color: #333333;\n}\n.skin-black .main-header > .navbar .sidebar-toggle:hover {\n  color: #999999;\n  background: #ffffff;\n}\n.skin-black .main-header > .navbar > .sidebar-toggle {\n  color: #333;\n  border-right: 1px solid #eee;\n}\n.skin-black .main-header > .navbar .navbar-nav > li > a {\n  border-right: 1px solid #eee;\n}\n.skin-black .main-header > .navbar .navbar-custom-menu .navbar-nav > li > a,\n.skin-black .main-header > .navbar .navbar-right > li > a {\n  border-left: 1px solid #eee;\n  border-right-width: 0;\n}\n.skin-black .main-header > .logo {\n  background-color: #ffffff;\n  color: #333333;\n  border-bottom: 0 solid transparent;\n  border-right: 1px solid #eee;\n}\n.skin-black .main-header > .logo:hover {\n  background-color: #fcfcfc;\n}\n@media (max-width: 767px) {\n  .skin-black .main-header > .logo {\n    background-color: #222222;\n    color: #ffffff;\n    border-bottom: 0 solid transparent;\n    border-right: none;\n  }\n  .skin-black .main-header > .logo:hover {\n    background-color: #1f1f1f;\n  }\n}\n.skin-black .main-header li.user-header {\n  background-color: #222;\n}\n.skin-black .content-header {\n  background: transparent;\n  box-shadow: none;\n}\n.skin-black .wrapper,\n.skin-black .main-sidebar,\n.skin-black .left-side {\n  background-color: #222d32;\n}\n.skin-black .user-panel > .info,\n.skin-black .user-panel > .info > a {\n  color: #fff;\n}\n.skin-black .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-black .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-black .sidebar-menu > li:hover > a,\n.skin-black .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #ffffff;\n}\n.skin-black .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-black .sidebar a {\n  color: #b8c7ce;\n}\n.skin-black .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-black .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-black .treeview-menu > li.active > a,\n.skin-black .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-black .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-black .sidebar-form input[type=\"text\"],\n.skin-black .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-black .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-black .sidebar-form input[type=\"text\"]:focus,\n.skin-black .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-black .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-black .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n.skin-black .pace .pace-progress {\n  background: #222;\n}\n.skin-black .pace .pace-activity {\n  border-top-color: #222;\n  border-left-color: #222;\n}\n/*\n * Skin: Black\n * -----------\n */\n/* skin-black navbar */\n.skin-black-light .main-header {\n  -webkit-box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05);\n  box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.05);\n}\n.skin-black-light .main-header .navbar-toggle {\n  color: #333;\n}\n.skin-black-light .main-header .navbar-brand {\n  color: #333;\n  border-right: 1px solid #eee;\n}\n.skin-black-light .main-header > .navbar {\n  background-color: #ffffff;\n}\n.skin-black-light .main-header > .navbar .nav > li > a {\n  color: #333333;\n}\n.skin-black-light .main-header > .navbar .nav > li > a:hover,\n.skin-black-light .main-header > .navbar .nav > li > a:active,\n.skin-black-light .main-header > .navbar .nav > li > a:focus,\n.skin-black-light .main-header > .navbar .nav .open > a,\n.skin-black-light .main-header > .navbar .nav .open > a:hover,\n.skin-black-light .main-header > .navbar .nav .open > a:focus,\n.skin-black-light .main-header > .navbar .nav > .active > a {\n  background: #ffffff;\n  color: #999999;\n}\n.skin-black-light .main-header > .navbar .sidebar-toggle {\n  color: #333333;\n}\n.skin-black-light .main-header > .navbar .sidebar-toggle:hover {\n  color: #999999;\n  background: #ffffff;\n}\n.skin-black-light .main-header > .navbar > .sidebar-toggle {\n  color: #333;\n  border-right: 1px solid #eee;\n}\n.skin-black-light .main-header > .navbar .navbar-nav > li > a {\n  border-right: 1px solid #eee;\n}\n.skin-black-light .main-header > .navbar .navbar-custom-menu .navbar-nav > li > a,\n.skin-black-light .main-header > .navbar .navbar-right > li > a {\n  border-left: 1px solid #eee;\n  border-right-width: 0;\n}\n.skin-black-light .main-header > .logo {\n  background-color: #ffffff;\n  color: #333333;\n  border-bottom: 0 solid transparent;\n  border-right: 1px solid #eee;\n}\n.skin-black-light .main-header > .logo:hover {\n  background-color: #fcfcfc;\n}\n@media (max-width: 767px) {\n  .skin-black-light .main-header > .logo {\n    background-color: #222222;\n    color: #ffffff;\n    border-bottom: 0 solid transparent;\n    border-right: none;\n  }\n  .skin-black-light .main-header > .logo:hover {\n    background-color: #1f1f1f;\n  }\n}\n.skin-black-light .main-header li.user-header {\n  background-color: #222;\n}\n.skin-black-light .content-header {\n  background: transparent;\n  box-shadow: none;\n}\n.skin-black-light .wrapper,\n.skin-black-light .main-sidebar,\n.skin-black-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-black-light .content-wrapper,\n.skin-black-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-black-light .user-panel > .info,\n.skin-black-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-black-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-black-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-black-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-black-light .sidebar-menu > li:hover > a,\n.skin-black-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-black-light .sidebar-menu > li.active {\n  border-left-color: #ffffff;\n}\n.skin-black-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-black-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-black-light .sidebar a {\n  color: #444444;\n}\n.skin-black-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-black-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-black-light .treeview-menu > li.active > a,\n.skin-black-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-black-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-black-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-black-light .sidebar-form input[type=\"text\"],\n.skin-black-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-black-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-black-light .sidebar-form input[type=\"text\"]:focus,\n.skin-black-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-black-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-black-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-black-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n/*\n * Skin: Green\n * -----------\n */\n.skin-green .main-header .navbar {\n  background-color: #00a65a;\n}\n.skin-green .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-green .main-header .navbar .nav > li > a:hover,\n.skin-green .main-header .navbar .nav > li > a:active,\n.skin-green .main-header .navbar .nav > li > a:focus,\n.skin-green .main-header .navbar .nav .open > a,\n.skin-green .main-header .navbar .nav .open > a:hover,\n.skin-green .main-header .navbar .nav .open > a:focus,\n.skin-green .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-green .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-green .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-green .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-green .main-header .navbar .sidebar-toggle:hover {\n  background-color: #008d4c;\n}\n@media (max-width: 767px) {\n  .skin-green .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-green .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-green .main-header .navbar .dropdown-menu li a:hover {\n    background: #008d4c;\n  }\n}\n.skin-green .main-header .logo {\n  background-color: #008d4c;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-green .main-header .logo:hover {\n  background-color: #008749;\n}\n.skin-green .main-header li.user-header {\n  background-color: #00a65a;\n}\n.skin-green .content-header {\n  background: transparent;\n}\n.skin-green .wrapper,\n.skin-green .main-sidebar,\n.skin-green .left-side {\n  background-color: #222d32;\n}\n.skin-green .user-panel > .info,\n.skin-green .user-panel > .info > a {\n  color: #fff;\n}\n.skin-green .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-green .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-green .sidebar-menu > li:hover > a,\n.skin-green .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #00a65a;\n}\n.skin-green .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-green .sidebar a {\n  color: #b8c7ce;\n}\n.skin-green .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-green .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-green .treeview-menu > li.active > a,\n.skin-green .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-green .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-green .sidebar-form input[type=\"text\"],\n.skin-green .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-green .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-green .sidebar-form input[type=\"text\"]:focus,\n.skin-green .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-green .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-green .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n/*\n * Skin: Green\n * -----------\n */\n.skin-green-light .main-header .navbar {\n  background-color: #00a65a;\n}\n.skin-green-light .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-green-light .main-header .navbar .nav > li > a:hover,\n.skin-green-light .main-header .navbar .nav > li > a:active,\n.skin-green-light .main-header .navbar .nav > li > a:focus,\n.skin-green-light .main-header .navbar .nav .open > a,\n.skin-green-light .main-header .navbar .nav .open > a:hover,\n.skin-green-light .main-header .navbar .nav .open > a:focus,\n.skin-green-light .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-green-light .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-green-light .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-green-light .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-green-light .main-header .navbar .sidebar-toggle:hover {\n  background-color: #008d4c;\n}\n@media (max-width: 767px) {\n  .skin-green-light .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-green-light .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-green-light .main-header .navbar .dropdown-menu li a:hover {\n    background: #008d4c;\n  }\n}\n.skin-green-light .main-header .logo {\n  background-color: #00a65a;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-green-light .main-header .logo:hover {\n  background-color: #00a157;\n}\n.skin-green-light .main-header li.user-header {\n  background-color: #00a65a;\n}\n.skin-green-light .content-header {\n  background: transparent;\n}\n.skin-green-light .wrapper,\n.skin-green-light .main-sidebar,\n.skin-green-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-green-light .content-wrapper,\n.skin-green-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-green-light .user-panel > .info,\n.skin-green-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-green-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-green-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-green-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-green-light .sidebar-menu > li:hover > a,\n.skin-green-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-green-light .sidebar-menu > li.active {\n  border-left-color: #00a65a;\n}\n.skin-green-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-green-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-green-light .sidebar a {\n  color: #444444;\n}\n.skin-green-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-green-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-green-light .treeview-menu > li.active > a,\n.skin-green-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-green-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-green-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-green-light .sidebar-form input[type=\"text\"],\n.skin-green-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-green-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-green-light .sidebar-form input[type=\"text\"]:focus,\n.skin-green-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-green-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-green-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-green-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n/*\n * Skin: Red\n * ---------\n */\n.skin-red .main-header .navbar {\n  background-color: #dd4b39;\n}\n.skin-red .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-red .main-header .navbar .nav > li > a:hover,\n.skin-red .main-header .navbar .nav > li > a:active,\n.skin-red .main-header .navbar .nav > li > a:focus,\n.skin-red .main-header .navbar .nav .open > a,\n.skin-red .main-header .navbar .nav .open > a:hover,\n.skin-red .main-header .navbar .nav .open > a:focus,\n.skin-red .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-red .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-red .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-red .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-red .main-header .navbar .sidebar-toggle:hover {\n  background-color: #d73925;\n}\n@media (max-width: 767px) {\n  .skin-red .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-red .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-red .main-header .navbar .dropdown-menu li a:hover {\n    background: #d73925;\n  }\n}\n.skin-red .main-header .logo {\n  background-color: #d73925;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-red .main-header .logo:hover {\n  background-color: #d33724;\n}\n.skin-red .main-header li.user-header {\n  background-color: #dd4b39;\n}\n.skin-red .content-header {\n  background: transparent;\n}\n.skin-red .wrapper,\n.skin-red .main-sidebar,\n.skin-red .left-side {\n  background-color: #222d32;\n}\n.skin-red .user-panel > .info,\n.skin-red .user-panel > .info > a {\n  color: #fff;\n}\n.skin-red .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-red .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-red .sidebar-menu > li:hover > a,\n.skin-red .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #dd4b39;\n}\n.skin-red .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-red .sidebar a {\n  color: #b8c7ce;\n}\n.skin-red .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-red .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-red .treeview-menu > li.active > a,\n.skin-red .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-red .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-red .sidebar-form input[type=\"text\"],\n.skin-red .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-red .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-red .sidebar-form input[type=\"text\"]:focus,\n.skin-red .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-red .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-red .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n/*\n * Skin: Red\n * ---------\n */\n.skin-red-light .main-header .navbar {\n  background-color: #dd4b39;\n}\n.skin-red-light .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-red-light .main-header .navbar .nav > li > a:hover,\n.skin-red-light .main-header .navbar .nav > li > a:active,\n.skin-red-light .main-header .navbar .nav > li > a:focus,\n.skin-red-light .main-header .navbar .nav .open > a,\n.skin-red-light .main-header .navbar .nav .open > a:hover,\n.skin-red-light .main-header .navbar .nav .open > a:focus,\n.skin-red-light .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-red-light .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-red-light .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-red-light .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-red-light .main-header .navbar .sidebar-toggle:hover {\n  background-color: #d73925;\n}\n@media (max-width: 767px) {\n  .skin-red-light .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-red-light .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-red-light .main-header .navbar .dropdown-menu li a:hover {\n    background: #d73925;\n  }\n}\n.skin-red-light .main-header .logo {\n  background-color: #dd4b39;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-red-light .main-header .logo:hover {\n  background-color: #dc4735;\n}\n.skin-red-light .main-header li.user-header {\n  background-color: #dd4b39;\n}\n.skin-red-light .content-header {\n  background: transparent;\n}\n.skin-red-light .wrapper,\n.skin-red-light .main-sidebar,\n.skin-red-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-red-light .content-wrapper,\n.skin-red-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-red-light .user-panel > .info,\n.skin-red-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-red-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-red-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-red-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-red-light .sidebar-menu > li:hover > a,\n.skin-red-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-red-light .sidebar-menu > li.active {\n  border-left-color: #dd4b39;\n}\n.skin-red-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-red-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-red-light .sidebar a {\n  color: #444444;\n}\n.skin-red-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-red-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-red-light .treeview-menu > li.active > a,\n.skin-red-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-red-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-red-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-red-light .sidebar-form input[type=\"text\"],\n.skin-red-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-red-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-red-light .sidebar-form input[type=\"text\"]:focus,\n.skin-red-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-red-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-red-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-red-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n/*\n * Skin: Yellow\n * ------------\n */\n.skin-yellow .main-header .navbar {\n  background-color: #f39c12;\n}\n.skin-yellow .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-yellow .main-header .navbar .nav > li > a:hover,\n.skin-yellow .main-header .navbar .nav > li > a:active,\n.skin-yellow .main-header .navbar .nav > li > a:focus,\n.skin-yellow .main-header .navbar .nav .open > a,\n.skin-yellow .main-header .navbar .nav .open > a:hover,\n.skin-yellow .main-header .navbar .nav .open > a:focus,\n.skin-yellow .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-yellow .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-yellow .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-yellow .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-yellow .main-header .navbar .sidebar-toggle:hover {\n  background-color: #e08e0b;\n}\n@media (max-width: 767px) {\n  .skin-yellow .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-yellow .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-yellow .main-header .navbar .dropdown-menu li a:hover {\n    background: #e08e0b;\n  }\n}\n.skin-yellow .main-header .logo {\n  background-color: #e08e0b;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-yellow .main-header .logo:hover {\n  background-color: #db8b0b;\n}\n.skin-yellow .main-header li.user-header {\n  background-color: #f39c12;\n}\n.skin-yellow .content-header {\n  background: transparent;\n}\n.skin-yellow .wrapper,\n.skin-yellow .main-sidebar,\n.skin-yellow .left-side {\n  background-color: #222d32;\n}\n.skin-yellow .user-panel > .info,\n.skin-yellow .user-panel > .info > a {\n  color: #fff;\n}\n.skin-yellow .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-yellow .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-yellow .sidebar-menu > li:hover > a,\n.skin-yellow .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #f39c12;\n}\n.skin-yellow .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-yellow .sidebar a {\n  color: #b8c7ce;\n}\n.skin-yellow .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-yellow .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-yellow .treeview-menu > li.active > a,\n.skin-yellow .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-yellow .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-yellow .sidebar-form input[type=\"text\"],\n.skin-yellow .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-yellow .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-yellow .sidebar-form input[type=\"text\"]:focus,\n.skin-yellow .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-yellow .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-yellow .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n/*\n * Skin: Yellow\n * ------------\n */\n.skin-yellow-light .main-header .navbar {\n  background-color: #f39c12;\n}\n.skin-yellow-light .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-yellow-light .main-header .navbar .nav > li > a:hover,\n.skin-yellow-light .main-header .navbar .nav > li > a:active,\n.skin-yellow-light .main-header .navbar .nav > li > a:focus,\n.skin-yellow-light .main-header .navbar .nav .open > a,\n.skin-yellow-light .main-header .navbar .nav .open > a:hover,\n.skin-yellow-light .main-header .navbar .nav .open > a:focus,\n.skin-yellow-light .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-yellow-light .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-yellow-light .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-yellow-light .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-yellow-light .main-header .navbar .sidebar-toggle:hover {\n  background-color: #e08e0b;\n}\n@media (max-width: 767px) {\n  .skin-yellow-light .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-yellow-light .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-yellow-light .main-header .navbar .dropdown-menu li a:hover {\n    background: #e08e0b;\n  }\n}\n.skin-yellow-light .main-header .logo {\n  background-color: #f39c12;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-yellow-light .main-header .logo:hover {\n  background-color: #f39a0d;\n}\n.skin-yellow-light .main-header li.user-header {\n  background-color: #f39c12;\n}\n.skin-yellow-light .content-header {\n  background: transparent;\n}\n.skin-yellow-light .wrapper,\n.skin-yellow-light .main-sidebar,\n.skin-yellow-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-yellow-light .content-wrapper,\n.skin-yellow-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-yellow-light .user-panel > .info,\n.skin-yellow-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-yellow-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-yellow-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-yellow-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-yellow-light .sidebar-menu > li:hover > a,\n.skin-yellow-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-yellow-light .sidebar-menu > li.active {\n  border-left-color: #f39c12;\n}\n.skin-yellow-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-yellow-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-yellow-light .sidebar a {\n  color: #444444;\n}\n.skin-yellow-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-yellow-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-yellow-light .treeview-menu > li.active > a,\n.skin-yellow-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-yellow-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-yellow-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-yellow-light .sidebar-form input[type=\"text\"],\n.skin-yellow-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-yellow-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-yellow-light .sidebar-form input[type=\"text\"]:focus,\n.skin-yellow-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-yellow-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-yellow-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-yellow-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n/*\n * Skin: Purple\n * ------------\n */\n.skin-purple .main-header .navbar {\n  background-color: #605ca8;\n}\n.skin-purple .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-purple .main-header .navbar .nav > li > a:hover,\n.skin-purple .main-header .navbar .nav > li > a:active,\n.skin-purple .main-header .navbar .nav > li > a:focus,\n.skin-purple .main-header .navbar .nav .open > a,\n.skin-purple .main-header .navbar .nav .open > a:hover,\n.skin-purple .main-header .navbar .nav .open > a:focus,\n.skin-purple .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-purple .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-purple .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-purple .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-purple .main-header .navbar .sidebar-toggle:hover {\n  background-color: #555299;\n}\n@media (max-width: 767px) {\n  .skin-purple .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-purple .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-purple .main-header .navbar .dropdown-menu li a:hover {\n    background: #555299;\n  }\n}\n.skin-purple .main-header .logo {\n  background-color: #555299;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-purple .main-header .logo:hover {\n  background-color: #545096;\n}\n.skin-purple .main-header li.user-header {\n  background-color: #605ca8;\n}\n.skin-purple .content-header {\n  background: transparent;\n}\n.skin-purple .wrapper,\n.skin-purple .main-sidebar,\n.skin-purple .left-side {\n  background-color: #222d32;\n}\n.skin-purple .user-panel > .info,\n.skin-purple .user-panel > .info > a {\n  color: #fff;\n}\n.skin-purple .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-purple .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-purple .sidebar-menu > li:hover > a,\n.skin-purple .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #605ca8;\n}\n.skin-purple .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-purple .sidebar a {\n  color: #b8c7ce;\n}\n.skin-purple .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-purple .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-purple .treeview-menu > li.active > a,\n.skin-purple .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-purple .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-purple .sidebar-form input[type=\"text\"],\n.skin-purple .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-purple .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-purple .sidebar-form input[type=\"text\"]:focus,\n.skin-purple .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-purple .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-purple .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n/*\n * Skin: Purple\n * ------------\n */\n.skin-purple-light .main-header .navbar {\n  background-color: #605ca8;\n}\n.skin-purple-light .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-purple-light .main-header .navbar .nav > li > a:hover,\n.skin-purple-light .main-header .navbar .nav > li > a:active,\n.skin-purple-light .main-header .navbar .nav > li > a:focus,\n.skin-purple-light .main-header .navbar .nav .open > a,\n.skin-purple-light .main-header .navbar .nav .open > a:hover,\n.skin-purple-light .main-header .navbar .nav .open > a:focus,\n.skin-purple-light .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-purple-light .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-purple-light .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-purple-light .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-purple-light .main-header .navbar .sidebar-toggle:hover {\n  background-color: #555299;\n}\n@media (max-width: 767px) {\n  .skin-purple-light .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-purple-light .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-purple-light .main-header .navbar .dropdown-menu li a:hover {\n    background: #555299;\n  }\n}\n.skin-purple-light .main-header .logo {\n  background-color: #605ca8;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-purple-light .main-header .logo:hover {\n  background-color: #5d59a6;\n}\n.skin-purple-light .main-header li.user-header {\n  background-color: #605ca8;\n}\n.skin-purple-light .content-header {\n  background: transparent;\n}\n.skin-purple-light .wrapper,\n.skin-purple-light .main-sidebar,\n.skin-purple-light .left-side {\n  background-color: #f9fafc;\n}\n.skin-purple-light .content-wrapper,\n.skin-purple-light .main-footer {\n  border-left: 1px solid #d2d6de;\n}\n.skin-purple-light .user-panel > .info,\n.skin-purple-light .user-panel > .info > a {\n  color: #444444;\n}\n.skin-purple-light .sidebar-menu > li {\n  -webkit-transition: border-left-color 0.3s ease;\n  -o-transition: border-left-color 0.3s ease;\n  transition: border-left-color 0.3s ease;\n}\n.skin-purple-light .sidebar-menu > li.header {\n  color: #848484;\n  background: #f9fafc;\n}\n.skin-purple-light .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n  font-weight: 600;\n}\n.skin-purple-light .sidebar-menu > li:hover > a,\n.skin-purple-light .sidebar-menu > li.active > a {\n  color: #000000;\n  background: #f4f4f5;\n}\n.skin-purple-light .sidebar-menu > li.active {\n  border-left-color: #605ca8;\n}\n.skin-purple-light .sidebar-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-purple-light .sidebar-menu > li > .treeview-menu {\n  background: #f4f4f5;\n}\n.skin-purple-light .sidebar a {\n  color: #444444;\n}\n.skin-purple-light .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-purple-light .treeview-menu > li > a {\n  color: #777777;\n}\n.skin-purple-light .treeview-menu > li.active > a,\n.skin-purple-light .treeview-menu > li > a:hover {\n  color: #000000;\n}\n.skin-purple-light .treeview-menu > li.active > a {\n  font-weight: 600;\n}\n.skin-purple-light .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #d2d6de;\n  margin: 10px 10px;\n}\n.skin-purple-light .sidebar-form input[type=\"text\"],\n.skin-purple-light .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #fff;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-purple-light .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-purple-light .sidebar-form input[type=\"text\"]:focus,\n.skin-purple-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-purple-light .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-purple-light .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n@media (min-width: 768px) {\n  .skin-purple-light.sidebar-mini.sidebar-collapse .sidebar-menu > li > .treeview-menu {\n    border-left: 1px solid #d2d6de;\n  }\n}\n"
  },
  {
    "path": "web/static/dist/css/skins/skin-blue.css",
    "content": "/*\n * Skin: Blue\n * ----------\n */\n.skin-blue .main-header .navbar {\n  background-color: #3c8dbc;\n}\n.skin-blue .main-header .navbar .nav > li > a {\n  color: #ffffff;\n}\n.skin-blue .main-header .navbar .nav > li > a:hover,\n.skin-blue .main-header .navbar .nav > li > a:active,\n.skin-blue .main-header .navbar .nav > li > a:focus,\n.skin-blue .main-header .navbar .nav .open > a,\n.skin-blue .main-header .navbar .nav .open > a:hover,\n.skin-blue .main-header .navbar .nav .open > a:focus,\n.skin-blue .main-header .navbar .nav > .active > a {\n  background: rgba(0, 0, 0, 0.1);\n  color: #f6f6f6;\n}\n.skin-blue .main-header .navbar .sidebar-toggle {\n  color: #ffffff;\n}\n.skin-blue .main-header .navbar .sidebar-toggle:hover {\n  color: #f6f6f6;\n  background: rgba(0, 0, 0, 0.1);\n}\n.skin-blue .main-header .navbar .sidebar-toggle {\n  color: #fff;\n}\n.skin-blue .main-header .navbar .sidebar-toggle:hover {\n  background-color: #367fa9;\n}\n@media (max-width: 767px) {\n  .skin-blue .main-header .navbar .dropdown-menu li.divider {\n    background-color: rgba(255, 255, 255, 0.1);\n  }\n  .skin-blue .main-header .navbar .dropdown-menu li a {\n    color: #fff;\n  }\n  .skin-blue .main-header .navbar .dropdown-menu li a:hover {\n    background: #367fa9;\n  }\n}\n.skin-blue .main-header .logo {\n  background-color: #367fa9;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue .main-header .logo:hover {\n  background-color: #357ca5;\n}\n.skin-blue .main-header li.user-header {\n  background-color: #3c8dbc;\n}\n.skin-blue .content-header {\n  background: transparent;\n}\n.skin-blue .wrapper,\n.skin-blue .main-sidebar,\n.skin-blue .left-side {\n  background-color: #222d32;\n}\n.skin-blue .user-panel > .info,\n.skin-blue .user-panel > .info > a {\n  color: #fff;\n}\n.skin-blue .sidebar-menu > li.header {\n  color: #4b646f;\n  background: #1a2226;\n}\n.skin-blue .sidebar-menu > li > a {\n  border-left: 3px solid transparent;\n}\n.skin-blue .sidebar-menu > li:hover > a,\n.skin-blue .sidebar-menu > li.active > a {\n  color: #ffffff;\n  background: #1e282c;\n  border-left-color: #3c8dbc;\n}\n.skin-blue .sidebar-menu > li > .treeview-menu {\n  margin: 0 1px;\n  background: #2c3b41;\n}\n.skin-blue .sidebar a {\n  color: #b8c7ce;\n}\n.skin-blue .sidebar a:hover {\n  text-decoration: none;\n}\n.skin-blue .treeview-menu > li > a {\n  color: #8aa4af;\n}\n.skin-blue .treeview-menu > li.active > a,\n.skin-blue .treeview-menu > li > a:hover {\n  color: #ffffff;\n}\n.skin-blue .sidebar-form {\n  border-radius: 3px;\n  border: 1px solid #374850;\n  margin: 10px 10px;\n}\n.skin-blue .sidebar-form input[type=\"text\"],\n.skin-blue .sidebar-form .btn {\n  box-shadow: none;\n  background-color: #374850;\n  border: 1px solid transparent;\n  height: 35px;\n  -webkit-transition: all 0.3s ease-in-out;\n  -o-transition: all 0.3s ease-in-out;\n  transition: all 0.3s ease-in-out;\n}\n.skin-blue .sidebar-form input[type=\"text\"] {\n  color: #666;\n  border-top-left-radius: 2px;\n  border-top-right-radius: 0;\n  border-bottom-right-radius: 0;\n  border-bottom-left-radius: 2px;\n}\n.skin-blue .sidebar-form input[type=\"text\"]:focus,\n.skin-blue .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  background-color: #fff;\n  color: #666;\n}\n.skin-blue .sidebar-form input[type=\"text\"]:focus + .input-group-btn .btn {\n  border-left-color: #fff;\n}\n.skin-blue .sidebar-form .btn {\n  color: #999;\n  border-top-left-radius: 0;\n  border-top-right-radius: 2px;\n  border-bottom-right-radius: 2px;\n  border-bottom-left-radius: 0;\n}\n.skin-blue.layout-top-nav .main-header > .logo {\n  background-color: #3c8dbc;\n  color: #ffffff;\n  border-bottom: 0 solid transparent;\n}\n.skin-blue.layout-top-nav .main-header > .logo:hover {\n  background-color: #3b8ab8;\n}\n"
  },
  {
    "path": "web/static/dist/js/app.js",
    "content": "/*! AdminLTE app.js\n * ================\n * Main JS application file for AdminLTE v2. This file\n * should be included in all pages. It controls some layout\n * options and implements exclusive AdminLTE plugins.\n *\n * @Author  Almsaeed Studio\n * @Support <http://www.almsaeedstudio.com>\n * @Email   <support@almsaeedstudio.com>\n * @version 2.3.2\n * @license MIT <http://opensource.org/licenses/MIT>\n */\n\n//Make sure jQuery has been loaded before app.js\nif (typeof jQuery === \"undefined\") {\n  throw new Error(\"AdminLTE requires jQuery\");\n}\n\n/* AdminLTE\n *\n * @type Object\n * @description $.AdminLTE is the main object for the template's app.\n *              It's used for implementing functions and options related\n *              to the template. Keeping everything wrapped in an object\n *              prevents conflict with other plugins and is a better\n *              way to organize our code.\n */\n$.AdminLTE = {};\n\n/* --------------------\n * - AdminLTE Options -\n * --------------------\n * Modify these options to suit your implementation\n */\n$.AdminLTE.options = {\n  //Add slimscroll to navbar menus\n  //This requires you to load the slimscroll plugin\n  //in every page before app.js\n  navbarMenuSlimscroll: true,\n  navbarMenuSlimscrollWidth: \"3px\", //The width of the scroll bar\n  navbarMenuHeight: \"200px\", //The height of the inner menu\n  //General animation speed for JS animated elements such as box collapse/expand and\n  //sidebar treeview slide up/down. This options accepts an integer as milliseconds,\n  //'fast', 'normal', or 'slow'\n  animationSpeed: 500,\n  //Sidebar push menu toggle button selector\n  sidebarToggleSelector: \"[data-toggle='offcanvas']\",\n  //Activate sidebar push menu\n  sidebarPushMenu: true,\n  //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin)\n  sidebarSlimScroll: true,\n  //Enable sidebar expand on hover effect for sidebar mini\n  //This option is forced to true if both the fixed layout and sidebar mini\n  //are used together\n  sidebarExpandOnHover: false,\n  //BoxRefresh Plugin\n  enableBoxRefresh: true,\n  //Bootstrap.js tooltip\n  enableBSToppltip: true,\n  BSTooltipSelector: \"[data-toggle='tooltip']\",\n  //Enable Fast Click. Fastclick.js creates a more\n  //native touch experience with touch devices. If you\n  //choose to enable the plugin, make sure you load the script\n  //before AdminLTE's app.js\n  enableFastclick: true,\n  //Control Sidebar Options\n  enableControlSidebar: true,\n  controlSidebarOptions: {\n    //Which button should trigger the open/close event\n    toggleBtnSelector: \"[data-toggle='control-sidebar']\",\n    //The sidebar selector\n    selector: \".control-sidebar\",\n    //Enable slide over content\n    slide: true\n  },\n  //Box Widget Plugin. Enable this plugin\n  //to allow boxes to be collapsed and/or removed\n  enableBoxWidget: true,\n  //Box Widget plugin options\n  boxWidgetOptions: {\n    boxWidgetIcons: {\n      //Collapse icon\n      collapse: 'fa-minus',\n      //Open icon\n      open: 'fa-plus',\n      //Remove icon\n      remove: 'fa-times'\n    },\n    boxWidgetSelectors: {\n      //Remove button selector\n      remove: '[data-widget=\"remove\"]',\n      //Collapse button selector\n      collapse: '[data-widget=\"collapse\"]'\n    }\n  },\n  //Direct Chat plugin options\n  directChat: {\n    //Enable direct chat by default\n    enable: true,\n    //The button to open and close the chat contacts pane\n    contactToggleSelector: '[data-widget=\"chat-pane-toggle\"]'\n  },\n  //Define the set of colors to use globally around the website\n  colors: {\n    lightBlue: \"#3c8dbc\",\n    red: \"#f56954\",\n    green: \"#00a65a\",\n    aqua: \"#00c0ef\",\n    yellow: \"#f39c12\",\n    blue: \"#0073b7\",\n    navy: \"#001F3F\",\n    teal: \"#39CCCC\",\n    olive: \"#3D9970\",\n    lime: \"#01FF70\",\n    orange: \"#FF851B\",\n    fuchsia: \"#F012BE\",\n    purple: \"#8E24AA\",\n    maroon: \"#D81B60\",\n    black: \"#222222\",\n    gray: \"#d2d6de\"\n  },\n  //The standard screen sizes that bootstrap uses.\n  //If you change these in the variables.less file, change\n  //them here too.\n  screenSizes: {\n    xs: 480,\n    sm: 768,\n    md: 992,\n    lg: 1200\n  }\n};\n\n/* ------------------\n * - Implementation -\n * ------------------\n * The next block of code implements AdminLTE's\n * functions and plugins as specified by the\n * options above.\n */\n$(function () {\n  \"use strict\";\n\n  //Fix for IE page transitions\n  $(\"body\").removeClass(\"hold-transition\");\n\n  //Extend options if external options exist\n  if (typeof AdminLTEOptions !== \"undefined\") {\n    $.extend(true,\n        $.AdminLTE.options,\n        AdminLTEOptions);\n  }\n\n  //Easy access to options\n  var o = $.AdminLTE.options;\n\n  //Set up the object\n  _init();\n\n  //Activate the layout maker\n  $.AdminLTE.layout.activate();\n\n  //Enable sidebar tree view controls\n  $.AdminLTE.tree('.sidebar');\n\n  //Enable control sidebar\n  if (o.enableControlSidebar) {\n    $.AdminLTE.controlSidebar.activate();\n  }\n\n  //Add slimscroll to navbar dropdown\n  if (o.navbarMenuSlimscroll && typeof $.fn.slimscroll != 'undefined') {\n    $(\".navbar .menu\").slimscroll({\n      height: o.navbarMenuHeight,\n      alwaysVisible: false,\n      size: o.navbarMenuSlimscrollWidth\n    }).css(\"width\", \"100%\");\n  }\n\n  //Activate sidebar push menu\n  if (o.sidebarPushMenu) {\n    $.AdminLTE.pushMenu.activate(o.sidebarToggleSelector);\n  }\n\n  //Activate Bootstrap tooltip\n  if (o.enableBSToppltip) {\n    $('body').tooltip({\n      selector: o.BSTooltipSelector\n    });\n  }\n\n  //Activate box widget\n  if (o.enableBoxWidget) {\n    $.AdminLTE.boxWidget.activate();\n  }\n\n  //Activate fast click\n  if (o.enableFastclick && typeof FastClick != 'undefined') {\n    FastClick.attach(document.body);\n  }\n\n  //Activate direct chat widget\n  if (o.directChat.enable) {\n    $(document).on('click', o.directChat.contactToggleSelector, function () {\n      var box = $(this).parents('.direct-chat').first();\n      box.toggleClass('direct-chat-contacts-open');\n    });\n  }\n\n  /*\n   * INITIALIZE BUTTON TOGGLE\n   * ------------------------\n   */\n  $('.btn-group[data-toggle=\"btn-toggle\"]').each(function () {\n    var group = $(this);\n    $(this).find(\".btn\").on('click', function (e) {\n      group.find(\".btn.active\").removeClass(\"active\");\n      $(this).addClass(\"active\");\n      e.preventDefault();\n    });\n\n  });\n});\n\n/* ----------------------------------\n * - Initialize the AdminLTE Object -\n * ----------------------------------\n * All AdminLTE functions are implemented below.\n */\nfunction _init() {\n  'use strict';\n  /* Layout\n   * ======\n   * Fixes the layout height in case min-height fails.\n   *\n   * @type Object\n   * @usage $.AdminLTE.layout.activate()\n   *        $.AdminLTE.layout.fix()\n   *        $.AdminLTE.layout.fixSidebar()\n   */\n  $.AdminLTE.layout = {\n    activate: function () {\n      var _this = this;\n      _this.fix();\n      _this.fixSidebar();\n      $(window, \".wrapper\").resize(function () {\n        _this.fix();\n        _this.fixSidebar();\n      });\n    },\n    fix: function () {\n      //Get window height and the wrapper height\n      var neg = $('.main-header').outerHeight() + $('.main-footer').outerHeight();\n      var window_height = $(window).height();\n      var sidebar_height = $(\".sidebar\").height();\n      //Set the min-height of the content and sidebar based on the\n      //the height of the document.\n      if ($(\"body\").hasClass(\"fixed\")) {\n        $(\".content-wrapper, .right-side\").css('min-height', window_height - $('.main-footer').outerHeight());\n      } else {\n        var postSetWidth;\n        if (window_height >= sidebar_height) {\n          $(\".content-wrapper, .right-side\").css('min-height', window_height - neg);\n          postSetWidth = window_height - neg;\n        } else {\n          $(\".content-wrapper, .right-side\").css('min-height', sidebar_height);\n          postSetWidth = sidebar_height;\n        }\n\n        //Fix for the control sidebar height\n        var controlSidebar = $($.AdminLTE.options.controlSidebarOptions.selector);\n        if (typeof controlSidebar !== \"undefined\") {\n          if (controlSidebar.height() > postSetWidth)\n            $(\".content-wrapper, .right-side\").css('min-height', controlSidebar.height());\n        }\n\n      }\n    },\n    fixSidebar: function () {\n      //Make sure the body tag has the .fixed class\n      if (!$(\"body\").hasClass(\"fixed\")) {\n        if (typeof $.fn.slimScroll != 'undefined') {\n          $(\".sidebar\").slimScroll({destroy: true}).height(\"auto\");\n        }\n        return;\n      } else if (typeof $.fn.slimScroll == 'undefined' && window.console) {\n        window.console.error(\"Error: the fixed layout requires the slimscroll plugin!\");\n      }\n      //Enable slimscroll for fixed layout\n      if ($.AdminLTE.options.sidebarSlimScroll) {\n        if (typeof $.fn.slimScroll != 'undefined') {\n          //Destroy if it exists\n          $(\".sidebar\").slimScroll({destroy: true}).height(\"auto\");\n          //Add slimscroll\n          $(\".sidebar\").slimscroll({\n            height: ($(window).height() - $(\".main-header\").height()) + \"px\",\n            color: \"rgba(0,0,0,0.2)\",\n            size: \"3px\"\n          });\n        }\n      }\n    }\n  };\n\n  /* PushMenu()\n   * ==========\n   * Adds the push menu functionality to the sidebar.\n   *\n   * @type Function\n   * @usage: $.AdminLTE.pushMenu(\"[data-toggle='offcanvas']\")\n   */\n  $.AdminLTE.pushMenu = {\n    activate: function (toggleBtn) {\n      //Get the screen sizes\n      var screenSizes = $.AdminLTE.options.screenSizes;\n\n      //Enable sidebar toggle\n      $(document).on('click', toggleBtn, function (e) {\n        e.preventDefault();\n\n        //Enable sidebar push menu\n        if ($(window).width() > (screenSizes.sm - 1)) {\n          if ($(\"body\").hasClass('sidebar-collapse')) {\n            $(\"body\").removeClass('sidebar-collapse').trigger('expanded.pushMenu');\n          } else {\n            $(\"body\").addClass('sidebar-collapse').trigger('collapsed.pushMenu');\n          }\n        }\n        //Handle sidebar push menu for small screens\n        else {\n          if ($(\"body\").hasClass('sidebar-open')) {\n            $(\"body\").removeClass('sidebar-open').removeClass('sidebar-collapse').trigger('collapsed.pushMenu');\n          } else {\n            $(\"body\").addClass('sidebar-open').trigger('expanded.pushMenu');\n          }\n        }\n      });\n\n      $(\".content-wrapper\").click(function () {\n        //Enable hide menu when clicking on the content-wrapper on small screens\n        if ($(window).width() <= (screenSizes.sm - 1) && $(\"body\").hasClass(\"sidebar-open\")) {\n          $(\"body\").removeClass('sidebar-open');\n        }\n      });\n\n      //Enable expand on hover for sidebar mini\n      if ($.AdminLTE.options.sidebarExpandOnHover\n          || ($('body').hasClass('fixed')\n          && $('body').hasClass('sidebar-mini'))) {\n        this.expandOnHover();\n      }\n    },\n    expandOnHover: function () {\n      var _this = this;\n      var screenWidth = $.AdminLTE.options.screenSizes.sm - 1;\n      //Expand sidebar on hover\n      $('.main-sidebar').hover(function () {\n        if ($('body').hasClass('sidebar-mini')\n            && $(\"body\").hasClass('sidebar-collapse')\n            && $(window).width() > screenWidth) {\n          _this.expand();\n        }\n      }, function () {\n        if ($('body').hasClass('sidebar-mini')\n            && $('body').hasClass('sidebar-expanded-on-hover')\n            && $(window).width() > screenWidth) {\n          _this.collapse();\n        }\n      });\n    },\n    expand: function () {\n      $(\"body\").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover');\n    },\n    collapse: function () {\n      if ($('body').hasClass('sidebar-expanded-on-hover')) {\n        $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse');\n      }\n    }\n  };\n\n  /* Tree()\n   * ======\n   * Converts the sidebar into a multilevel\n   * tree view menu.\n   *\n   * @type Function\n   * @Usage: $.AdminLTE.tree('.sidebar')\n   */\n  $.AdminLTE.tree = function (menu) {\n    var _this = this;\n    var animationSpeed = $.AdminLTE.options.animationSpeed;\n    $(document).on('click', menu + ' li a', function (e) {\n      //Get the clicked link and the next element\n      var $this = $(this);\n      var checkElement = $this.next();\n\n      //Check if the next element is a menu and is visible\n      if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) {\n        //Close the menu\n        checkElement.slideUp(animationSpeed, function () {\n          checkElement.removeClass('menu-open');\n          //Fix the layout in case the sidebar stretches over the height of the window\n          //_this.layout.fix();\n        });\n        checkElement.parent(\"li\").removeClass(\"active\");\n      }\n      //If the menu is not visible\n      else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) {\n        //Get the parent menu\n        var parent = $this.parents('ul').first();\n        //Close all open menus within the parent\n        var ul = parent.find('ul:visible').slideUp(animationSpeed);\n        //Remove the menu-open class from the parent\n        ul.removeClass('menu-open');\n        //Get the parent li\n        var parent_li = $this.parent(\"li\");\n\n        //Open the target menu and add the menu-open class\n        checkElement.slideDown(animationSpeed, function () {\n          //Add the class active to the parent li\n          checkElement.addClass('menu-open');\n          parent.find('li.active').removeClass('active');\n          parent_li.addClass('active');\n          //Fix the layout in case the sidebar stretches over the height of the window\n          _this.layout.fix();\n        });\n      }\n      //if this isn't a link, prevent the page from being redirected\n      if (checkElement.is('.treeview-menu')) {\n        e.preventDefault();\n      }\n    });\n  };\n\n  /* ControlSidebar\n   * ==============\n   * Adds functionality to the right sidebar\n   *\n   * @type Object\n   * @usage $.AdminLTE.controlSidebar.activate(options)\n   */\n  $.AdminLTE.controlSidebar = {\n    //instantiate the object\n    activate: function () {\n      //Get the object\n      var _this = this;\n      //Update options\n      var o = $.AdminLTE.options.controlSidebarOptions;\n      //Get the sidebar\n      var sidebar = $(o.selector);\n      //The toggle button\n      var btn = $(o.toggleBtnSelector);\n\n      //Listen to the click event\n      btn.on('click', function (e) {\n        e.preventDefault();\n        //If the sidebar is not open\n        if (!sidebar.hasClass('control-sidebar-open')\n            && !$('body').hasClass('control-sidebar-open')) {\n          //Open the sidebar\n          _this.open(sidebar, o.slide);\n        } else {\n          _this.close(sidebar, o.slide);\n        }\n      });\n\n      //If the body has a boxed layout, fix the sidebar bg position\n      var bg = $(\".control-sidebar-bg\");\n      _this._fix(bg);\n\n      //If the body has a fixed layout, make the control sidebar fixed\n      if ($('body').hasClass('fixed')) {\n        _this._fixForFixed(sidebar);\n      } else {\n        //If the content height is less than the sidebar's height, force max height\n        if ($('.content-wrapper, .right-side').height() < sidebar.height()) {\n          _this._fixForContent(sidebar);\n        }\n      }\n    },\n    //Open the control sidebar\n    open: function (sidebar, slide) {\n      //Slide over content\n      if (slide) {\n        sidebar.addClass('control-sidebar-open');\n      } else {\n        //Push the content by adding the open class to the body instead\n        //of the sidebar itself\n        $('body').addClass('control-sidebar-open');\n      }\n    },\n    //Close the control sidebar\n    close: function (sidebar, slide) {\n      if (slide) {\n        sidebar.removeClass('control-sidebar-open');\n      } else {\n        $('body').removeClass('control-sidebar-open');\n      }\n    },\n    _fix: function (sidebar) {\n      var _this = this;\n      if ($(\"body\").hasClass('layout-boxed')) {\n        sidebar.css('position', 'absolute');\n        sidebar.height($(\".wrapper\").height());\n        $(window).resize(function () {\n          _this._fix(sidebar);\n        });\n      } else {\n        sidebar.css({\n          'position': 'fixed',\n          'height': 'auto'\n        });\n      }\n    },\n    _fixForFixed: function (sidebar) {\n      sidebar.css({\n        'position': 'fixed',\n        'max-height': '100%',\n        'overflow': 'auto',\n        'padding-bottom': '50px'\n      });\n    },\n    _fixForContent: function (sidebar) {\n      $(\".content-wrapper, .right-side\").css('min-height', sidebar.height());\n    }\n  };\n\n  /* BoxWidget\n   * =========\n   * BoxWidget is a plugin to handle collapsing and\n   * removing boxes from the screen.\n   *\n   * @type Object\n   * @usage $.AdminLTE.boxWidget.activate()\n   *        Set all your options in the main $.AdminLTE.options object\n   */\n  $.AdminLTE.boxWidget = {\n    selectors: $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors,\n    icons: $.AdminLTE.options.boxWidgetOptions.boxWidgetIcons,\n    animationSpeed: $.AdminLTE.options.animationSpeed,\n    activate: function (_box) {\n      var _this = this;\n      if (!_box) {\n        _box = document; // activate all boxes per default\n      }\n      //Listen for collapse event triggers\n      $(_box).on('click', _this.selectors.collapse, function (e) {\n        e.preventDefault();\n        _this.collapse($(this));\n      });\n\n      //Listen for remove event triggers\n      $(_box).on('click', _this.selectors.remove, function (e) {\n        e.preventDefault();\n        _this.remove($(this));\n      });\n    },\n    collapse: function (element) {\n      var _this = this;\n      //Find the box parent\n      var box = element.parents(\".box\").first();\n      //Find the body and the footer\n      var box_content = box.find(\"> .box-body, > .box-footer, > form  >.box-body, > form > .box-footer\");\n      if (!box.hasClass(\"collapsed-box\")) {\n        //Convert minus into plus\n        element.children(\":first\")\n            .removeClass(_this.icons.collapse)\n            .addClass(_this.icons.open);\n        //Hide the content\n        box_content.slideUp(_this.animationSpeed, function () {\n          box.addClass(\"collapsed-box\");\n        });\n      } else {\n        //Convert plus into minus\n        element.children(\":first\")\n            .removeClass(_this.icons.open)\n            .addClass(_this.icons.collapse);\n        //Show the content\n        box_content.slideDown(_this.animationSpeed, function () {\n          box.removeClass(\"collapsed-box\");\n        });\n      }\n    },\n    remove: function (element) {\n      //Find the box parent\n      var box = element.parents(\".box\").first();\n      box.slideUp(this.animationSpeed);\n    }\n  };\n}\n\n/* ------------------\n * - Custom Plugins -\n * ------------------\n * All custom plugins are defined below.\n */\n\n/*\n * BOX REFRESH BUTTON\n * ------------------\n * This is a custom plugin to use with the component BOX. It allows you to add\n * a refresh button to the box. It converts the box's state to a loading state.\n *\n * @type plugin\n * @usage $(\"#box-widget\").boxRefresh( options );\n */\n(function ($) {\n\n  \"use strict\";\n\n  $.fn.boxRefresh = function (options) {\n\n    // Render options\n    var settings = $.extend({\n      //Refresh button selector\n      trigger: \".refresh-btn\",\n      //File source to be loaded (e.g: ajax/src.php)\n      source: \"\",\n      //Callbacks\n      onLoadStart: function (box) {\n        return box;\n      }, //Right after the button has been clicked\n      onLoadDone: function (box) {\n        return box;\n      } //When the source has been loaded\n\n    }, options);\n\n    //The overlay\n    var overlay = $('<div class=\"overlay\"><div class=\"fa fa-refresh fa-spin\"></div></div>');\n\n    return this.each(function () {\n      //if a source is specified\n      if (settings.source === \"\") {\n        if (window.console) {\n          window.console.log(\"Please specify a source first - boxRefresh()\");\n        }\n        return;\n      }\n      //the box\n      var box = $(this);\n      //the button\n      var rBtn = box.find(settings.trigger).first();\n\n      //On trigger click\n      rBtn.on('click', function (e) {\n        e.preventDefault();\n        //Add loading overlay\n        start(box);\n\n        //Perform ajax call\n        box.find(\".box-body\").load(settings.source, function () {\n          done(box);\n        });\n      });\n    });\n\n    function start(box) {\n      //Add overlay and loading img\n      box.append(overlay);\n\n      settings.onLoadStart.call(box);\n    }\n\n    function done(box) {\n      //Remove overlay and loading img\n      box.find(overlay).remove();\n\n      settings.onLoadDone.call(box);\n    }\n\n  };\n\n})(jQuery);\n\n /*\n * EXPLICIT BOX CONTROLS\n * -----------------------\n * This is a custom plugin to use with the component BOX. It allows you to activate\n * a box inserted in the DOM after the app.js was loaded, toggle and remove box.\n *\n * @type plugin\n * @usage $(\"#box-widget\").activateBox();\n * @usage $(\"#box-widget\").toggleBox();\n * @usage $(\"#box-widget\").removeBox();\n */\n(function ($) {\n\n  'use strict';\n\n  $.fn.activateBox = function () {\n    $.AdminLTE.boxWidget.activate(this);\n  };\n\n  $.fn.toggleBox = function(){\n    var button = $($.AdminLTE.boxWidget.selectors.collapse, this);\n    $.AdminLTE.boxWidget.collapse(button);\n  };\n\n  $.fn.removeBox = function(){\n    var button = $($.AdminLTE.boxWidget.selectors.remove, this);\n    $.AdminLTE.boxWidget.remove(button);\n  };\n\n})(jQuery);\n\n/*\n * TODO LIST CUSTOM PLUGIN\n * -----------------------\n * This plugin depends on iCheck plugin for checkbox and radio inputs\n *\n * @type plugin\n * @usage $(\"#todo-widget\").todolist( options );\n */\n(function ($) {\n\n  'use strict';\n\n  $.fn.todolist = function (options) {\n    // Render options\n    var settings = $.extend({\n      //When the user checks the input\n      onCheck: function (ele) {\n        return ele;\n      },\n      //When the user unchecks the input\n      onUncheck: function (ele) {\n        return ele;\n      }\n    }, options);\n\n    return this.each(function () {\n\n      if (typeof $.fn.iCheck != 'undefined') {\n        $('input', this).on('ifChecked', function () {\n          var ele = $(this).parents(\"li\").first();\n          ele.toggleClass(\"done\");\n          settings.onCheck.call(ele);\n        });\n\n        $('input', this).on('ifUnchecked', function () {\n          var ele = $(this).parents(\"li\").first();\n          ele.toggleClass(\"done\");\n          settings.onUncheck.call(ele);\n        });\n      } else {\n        $('input', this).on('change', function () {\n          var ele = $(this).parents(\"li\").first();\n          ele.toggleClass(\"done\");\n          if ($('input', ele).is(\":checked\")) {\n            settings.onCheck.call(ele);\n          } else {\n            settings.onUncheck.call(ele);\n          }\n        });\n      }\n    });\n  };\n}(jQuery));\n"
  },
  {
    "path": "web/static/js/plot_monitor.js",
    "content": "var mem_usedp = 0;\nvar cpu_usedp = 0;\nvar is_running = true;\nvar ingress_rate = 0;\nvar egress_rate = 0;\nvar ingress_rate_limit = 0;\nvar egress_rate_limit = 0;\n\nfunction processMemData(data)\n{\n}\nfunction getMemY()\n{\n\treturn mem_usedp*100;\n}\nfunction processCpuData(data)\n{\n}\nfunction getCpuY()\n{\n\treturn cpu_usedp*100;\n}\n\nfunction processRate(data)\n{\n}\nfunction getIngressRateP()\n{\n  //alert(ingress_rate*8 / 1000.0);\n  return ingress_rate * 8 / 1000.0;\n}\nfunction getEgressRateP()\n{\n  return egress_rate * 8 / 1000.0;\n}\n\nfunction plot_graph(container,url,processData,getY,fetchdata=true, maxy=110) {\n\n    //var container = $(\"#flot-line-chart-moving\");\n\n    // Determine how many data points to keep based on the placeholder's initial size;\n    // this gives us a nice high-res plot while avoiding more than one point per pixel.\n\n    var maximum = container.outerWidth() / 2 || 300;\n\n    //\n\n    var data = [];\n\n\n\n    function getBaseData() {\n\n        while (data.length < maximum) {\n           data.push(0)\n        }\n\n        // zip the generated y values with the x values\n\n        var res = [];\n        for (var i = 0; i < data.length; ++i) {\n            res.push([i, data[i]])\n        }\n\n        return res;\n    }\n\n    function getData() {\n\n        if (data.length) {\n            data = data.slice(1);\n        }\n\n        if (data.length < maximum) {\n            if(fetchdata)\n                $.post(url,{},processData,\"json\");\n\t          var y = getY();\n            data.push(y < 0 ? 0 : y > maxy ? maxy : y);\n        }\n\n        // zip the generated y values with the x values\n\n        var res = [];\n        for (var i = 0; i < data.length; ++i) {\n            res.push([i, data[i]])\n        }\n\n        return res;\n    }\n\n\n\n    series = [{\n        data: getBaseData(),\n        lines: {\n            fill: true\n        }\n    }];\n\n\n    var plot = $.plot(container, series, {\n        grid: {\n\n            color: \"#999999\",\n            tickColor: \"#D4D4D4\",\n            borderWidth:0,\n            minBorderMargin: 20,\n            labelMargin: 10,\n            backgroundColor: {\n                colors: [\"#ffffff\", \"#ffffff\"]\n            },\n            margin: {\n                top: 8,\n                bottom: 20,\n                left: 20\n            },\n            markings: function(axes) {\n                var markings = [];\n                var xaxis = axes.xaxis;\n                for (var x = Math.floor(xaxis.min); x < xaxis.max; x += xaxis.tickSize * 2) {\n                    markings.push({\n                        xaxis: {\n                            from: x,\n                            to: x + xaxis.tickSize\n                        },\n                        color: \"#fff\"\n                    });\n                }\n                return markings;\n            }\n        },\n        colors: [\"#1ab394\"],\n        xaxis: {\n            tickFormatter: function() {\n                return \"\";\n            }\n        },\n        yaxis: {\n            min: 0,\n            max: maxy\n        },\n        legend: {\n            show: true\n        }\n    });\n\n    // Update the random dataset at 25FPS for a smoothly-animating chart\n\n    setInterval(function updateRandom() {\n        series[0].data = getData();\n        plot.setData(series);\n        plot.draw();\n    }, 1000);\n\n}\n\n\nvar host = window.location.host;\n\nvar node_name = $(\"#node_name\").html();\nvar masterip = $(\"#masterip\").html();\nvar url = \"//\" + host + \"/monitor/\" + masterip + \"/vnodes/\" + node_name;\n\nfunction num2human(data)\n{\n   units=['','K','M','G','T'];\n   tempdata = data/1.0;\n   //return tempdata;\n   for(var i = 1; i < units.length; ++i)\n   {\n      if( tempdata / 1000.0 > 1)\n          tempdata = tempdata/1000.0;\n      else\n          return tempdata.toFixed(2) + units[i-1];\n   }\n   return tempdata.toFixed(2) + units[4];\n}\n\nfunction processInfo()\n{\n    $.post(url+\"/info/\",{},function(data){\n        basic_info = data.monitor.basic_info;\n        state = basic_info.State;\n        if(state == 'STOPPED')\n        {\n            is_running = false;\n            $(\"#con_state\").html(\"<div class='label label-danger'>Stopped</div>\");\n            $(\"#con_ip\").html(\"--\");\n        }\n        else\n        {\n            is_running = true;\n            $(\"#con_state\").html(\"<div class='label label-primary'>Running</div>\");\n            $(\"#con_ip\").html(basic_info.IP);\n        }\n        var total = parseInt(basic_info.RunningTime);\n        var hour = Math.floor(total / 3600);\n        var min = Math.floor(total % 3600 / 60);\n        var secs = Math.floor(total % 3600 % 60);\n        $(\"#con_time\").html(hour+\"h \"+min+\"m \"+secs+\"s\");\n        $(\"#con_billing\").html(\"<a target='_blank' title='How to figure out it?' href='https://unias.github.io/docklet/book/en/billing/billing.html'>\"+basic_info.billing+\" <img src='/static/img/bean.png' /></a>\");\n        $(\"#con_billingthishour\").html(\"<a target='_blank' title='How to figure out it?' href='https://unias.github.io/docklet/book/en/billing/billing.html'>\"+basic_info.billing_this_hour.total+\" <img src='/static/img/bean.png' /></a>\");\n\n        if(is_running)\n        {\n    \t    cpu_usedp = data.monitor.cpu_use.usedp;\n    \t    var val = (data.monitor.cpu_use.val).toFixed(2);\n    \t    var unit = data.monitor.cpu_use.unit;\n            var quota = data.monitor.cpu_use.quota.cpu;\n            var quotaout = \"(\"+quota;\n            if(quota == 1)\n                quotaout += \" Core)\";\n            else\n                quotaout += \" Cores)\";\n    \t    $(\"#con_cpu\").html(val +\" \"+ unit+\"<br/>\"+quotaout);\n\n          mem_usedp = data.monitor.mem_use.usedp;\n          var usedp = data.monitor.mem_use.usedp;\n          unit = data.monitor.mem_use.unit;\n          var quota = data.monitor.mem_use.quota.memory/1024.0;\n          val = data.monitor.mem_use.val;\n          var out = \"(\"+val+unit+\"/\"+quota.toFixed(2)+\"MiB)\";\n          $(\"#con_mem\").html((usedp/0.01).toFixed(2)+\"%<br/>\"+out);\n        }\n        else\n        {\n            cpu_usedp = 0;\n            $(\"#con_cpu\").html(\"--\");\n\n            mem_usedp = 0;\n            $(\"#con_mem\").html(\"--\");\n        }\n\n        //processDiskData\n        var diskuse = data.monitor.disk_use;\n        var usedp = diskuse.percent;\n        var total = diskuse.total/1024.0/1024.0;\n        var used = diskuse.used/1024.0/1024.0;\n        var detail = \"(\"+used.toFixed(2)+\"MiB/\"+total.toFixed(2)+\"MiB)\";\n        $(\"#con_disk\").html(usedp+\"%<br/>\"+detail);\n\n        //processNetStats\n\t      var net_stats = data.monitor.net_stats;\n\t\t\t\tif(!$.isEmptyObject(net_stats))\n\t\t\t\t{\n\t        var in_rate = parseInt(net_stats.bytes_recv_per_sec);\n\t        var out_rate = parseInt(net_stats.bytes_sent_per_sec);\n\t        ingress_rate = in_rate;\n\t        egress_rate = out_rate;\n\t        $(\"#net_in_rate\").html(num2human(in_rate)+\"Bps\");\n\t        $(\"#net_out_rate\").html(num2human(out_rate)+\"Bps\");\n\t        $(\"#net_in_bytes\").html(num2human(net_stats.bytes_recv)+\"B\");\n\t        $(\"#net_out_bytes\").html(num2human(net_stats.bytes_sent)+\"B\");\n\t        $(\"#net_in_packs\").html(net_stats.packets_recv);\n\t        $(\"#net_out_packs\").html(net_stats.packets_sent);\n\t        $(\"#net_in_err\").html(net_stats.errout);\n\t        $(\"#net_out_err\").html(net_stats.errin);\n\t        $(\"#net_in_drop\").html(net_stats.dropout);\n\t        $(\"#net_out_drop\").html(net_stats.dropin);\n\t\t\t\t}\n\t\t\t\telse {\n\t\t\t\t\tingress_rate = 0;\n\t\t\t\t\tegress_rate = 0;\n\t\t\t\t\t$(\"#net_in_rate\").html(\"--\");\n\t\t\t\t\t$(\"#net_out_rate\").html(\"--\");\n\t\t\t\t\t$(\"#net_in_bytes\").html(\"--\");\n\t\t\t\t\t$(\"#net_out_bytes\").html(\"--\");\n\t\t\t\t\t$(\"#net_in_packs\").html(\"--\");\n\t\t\t\t\t$(\"#net_out_packs\").html(\"--\");\n\t\t\t\t\t$(\"#net_in_err\").html(\"--\");\n\t\t\t\t\t$(\"#net_out_err\").html(\"--\");\n\t\t\t\t\t$(\"#net_in_drop\").html(\"--\");\n\t\t\t\t\t$(\"#net_out_drop\").html(\"--\");\n\t\t\t\t}\n    },\"json\");\n}\n\nfunction plot_net(host,monitorurl)\n{\n  var url = \"//\" + host + \"/user/selfQuery/\";\n\n   $.post(url,{},function(data){\n      ingress_rate_limit = parseInt(data.groupinfo.input_rate_limit);\n      egress_rate_limit = parseInt(data.groupinfo.output_rate_limit);\n      if(ingress_rate_limit == 0)\n        ingress_rate_limit = egress_rate_limit*1.5;\n      plot_graph($(\"#ingress-chart\"), monitorurl, processRate, getIngressRateP,false,ingress_rate_limit);\n      plot_graph($(\"#egress-chart\"), monitorurl, processRate, getEgressRateP,false,egress_rate_limit*1.5);\n   },\"json\");\n}\n\nsetInterval(processInfo,1000);\nplot_graph($(\"#mem-chart\"),url + \"/mem_use/\",processMemData,getMemY,false);\nplot_graph($(\"#cpu-chart\"),url + \"/cpu_use/\",processCpuData,getCpuY,false);\nplot_net(host, url + \"/net_stats/\");\n"
  },
  {
    "path": "web/static/js/plot_monitorReal.js",
    "content": "\nvar used = 0;\nvar total = 0;\nvar idle = 0;\nvar disk_usedp = 0;\nvar count = 0;\nvar Ki = 1024;\nvar is_running = true;\n\nfunction processMemData(data)\n{\n    if(is_running)\n    {\n\t    used = data.monitor.meminfo.used;\n\t    total = data.monitor.meminfo.total;\n\t    var used2 = ((data.monitor.meminfo.used)/Ki).toFixed(2);\n\t    var total2 = ((data.monitor.meminfo.total)/Ki).toFixed(2);\n\t    var free2 = ((data.monitor.meminfo.free)/Ki).toFixed(2);\n\t    $(\"#mem_used\").html(used2);\n\t    $(\"#mem_total\").html(total2);\n\t    $(\"#mem_free\").html(free2);\n    }\n    else\n    {\n        total = 0;\n\t    $(\"#mem_used\").html(\"--\");\n\t    $(\"#mem_total\").html(\"--\");\n\t    $(\"#mem_free\").html(\"--\");\n    }\n}\nfunction getMemY()\n{\n\tif(total == 0)\n\t\treturn 0;\n\telse\n\t\treturn (used/total)*100;\n}\nfunction processCpuData(data)\n{\n    if(is_running)\n    {\n\t    idle = data.monitor.cpuinfo.idle;\n\t    var us = data.monitor.cpuinfo.user;\n\t    var sy = data.monitor.cpuinfo.system;\n\t    var wa = data.monitor.cpuinfo.iowait;\n\t    $(\"#cpu_user\").html(us);\n\t    $(\"#cpu_system\").html(sy);\n\t    $(\"#cpu_iowait\").html(wa);\n\t    $(\"#cpu_idle\").html(idle);\n    }\n    else\n    {\n        idle = 100;\n\t    $(\"#cpu_user\").html(\"--\");\n\t    $(\"#cpu_system\").html(\"--\");\n\t    $(\"#cpu_iowait\").html(\"--\");\n\t    $(\"#cpu_idle\").html(\"--\");\n    }\n}\nfunction getCpuY()\n{\n\tcount++;\n\t//alert(idle);\n\tif(count <= 3 && idle <= 10)\n\t\treturn 0;\n\telse\n\t\treturn (100-idle);\n}\nfunction processDiskData(data)\n{\n\tvar vals = data.monitor.diskinfo;\n\tdisk_usedp = vals[0].usedp;\n\tfor(var idx = 0; idx < vals.length; ++idx)\n\t{\n\t\tvar used = (vals[idx].used/Ki/Ki).toFixed(2);\n\t\tvar total = (vals[idx].total/Ki/Ki).toFixed(2);\n\t\tvar free = (vals[idx].free/Ki/Ki).toFixed(2);\n\t\tvar usedp = (vals[idx].percent);\n\t\tvar name = \"#disk_\" + (idx+1) + \"_\";\n\t\t$(name+\"device\").html(vals[idx].device);\n\t\t$(name+\"used\").html(used);\n\t\t$(name+\"total\").html(total);\n\t\t$(name+\"free\").html(free);\n\t\t$(name+\"usedp\").html(usedp);\n\t}\n}\nfunction getDiskY()\n{\n\treturn disk_usedp;\n}\n\nfunction plot_graph(container,url,processData,getY) {\n\n    //var container = $(\"#flot-line-chart-moving\");\n\n    // Determine how many data points to keep based on the placeholder's initial size;\n    // this gives us a nice high-res plot while avoiding more than one point per pixel.\n\n    var maximum = container.outerWidth() / 2 || 300;\n\n    //\n\n    var data = [];\n\n\n\n    function getBaseData() {\n\n        while (data.length < maximum) {\n           data.push(0)\n        }\n\n        // zip the generated y values with the x values\n\n        var res = [];\n        for (var i = 0; i < data.length; ++i) {\n            res.push([i, data[i]])\n        }\n\n        return res;\n    }\n\n    function getData() {\n\n        if (data.length) {\n            data = data.slice(1);\n        }\n\n        if (data.length < maximum) {\n            $.post(url,{user:\"root\",key:\"unias\"},processData,\"json\");\n\t    var y = getY();\n            data.push(y < 0 ? 0 : y > 100 ? 100 : y);\n        }\n\n        // zip the generated y values with the x values\n\n        var res = [];\n        for (var i = 0; i < data.length; ++i) {\n            res.push([i, data[i]])\n        }\n\n        return res;\n    }\n\n\n\n    series = [{\n        data: getBaseData(),\n        lines: {\n            fill: true\n        }\n    }];\n\n\n    var plot = $.plot(container, series, {\n        grid: {\n\n            color: \"#999999\",\n            tickColor: \"#D4D4D4\",\n            borderWidth:0,\n            minBorderMargin: 20,\n            labelMargin: 10,\n            backgroundColor: {\n                colors: [\"#ffffff\", \"#ffffff\"]\n            },\n            margin: {\n                top: 8,\n                bottom: 20,\n                left: 20\n            },\n            markings: function(axes) {\n                var markings = [];\n                var xaxis = axes.xaxis;\n                for (var x = Math.floor(xaxis.min); x < xaxis.max; x += xaxis.tickSize * 2) {\n                    markings.push({\n                        xaxis: {\n                            from: x,\n                            to: x + xaxis.tickSize\n                        },\n                        color: \"#fff\"\n                    });\n                }\n                return markings;\n            }\n        },\n        colors: [\"#1ab394\"],\n        xaxis: {\n            tickFormatter: function() {\n                return \"\";\n            }\n        },\n        yaxis: {\n            min: 0,\n            max: 110\n        },\n        legend: {\n            show: true\n        }\n    });\n\n    // Update the random dataset at 25FPS for a smoothly-animating chart\n\n    setInterval(function updateRandom() {\n        series[0].data = getData();\n        plot.setData(series);\n        plot.draw();\n    }, 1000);\n\n}\nvar host = window.location.host;\n\nvar com_ip = $(\"#com_ip\").html();\nvar masterip = $(\"#masterip\").html();\nvar url = \"//\" + host + \"/monitor/\" + masterip + \"/hosts/\"+com_ip;\n\nfunction processStatus()\n{\n    $.post(url+\"/status/\",{},function(data){\n        var state = data.monitor.status;\n        if(state == 'RUNNING')\n            is_running = true;\n        else\n            is_running = false;\n    },\"json\");\n}\nsetInterval(processStatus,1000);\n\nplot_graph($(\"#mem-chart\"), url + \"/meminfo/\" ,processMemData,getMemY);\nplot_graph($(\"#cpu-chart\"), url +  \"/cpuinfo/\",processCpuData,getCpuY);\n//plot_graph($(\"#disk-chart\"), url + \"/diskinfo\",processDiskData,getDiskY);\n$.post(url+\"/diskinfo/\",{},processDiskData,\"json\");\n"
  },
  {
    "path": "web/templates/addCluster.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Create Workspace{% endblock %}\n\n{% block css_src %}\n<!--<style>\n.divcontent { overflow-y:scroll; height:200px;}\n</style>-->\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n{% block panel_title %}Workspace Info{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n</ol>\n{% endblock %}\n\n<div>\n{% block content %}\n<div class=\"row\">\n                <div class=\"col-lg-12\">\n                  <div class=\"box box-info\">\n                       <div class=\"box-header with-border\">\n                         <h3 class=\"box-title\">Workspace Add</h3>\n\n                         <div class=\"box-tools pull-right\">\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                           </button>\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                         </div>\n                       </div>\n                        <div class=\"box-body\">\n\t\t\t\t<form id=\"form\" class=\"form-horizontal\" action=\"/workspace/{{masterips[0].split(\"@\")[0]}}/add/\" method=\"POST\">\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n\t\t\t\t    <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Workspace Name</label>\n\t\t\t\t\t    <div class=\"col-sm-10\"><input type=\"text\" class=\"form-control\" name=\"clusterName\" id=\"clusterName\"></div>\n\t\t\t\t    </div>\n\t\t\t\t    <div class=\"hr-line-dashed\"></div>\n                    <br/>\n\t\t    \t\t    <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Location</label>\n\t\t\t\t\t    <div class=\"col-sm-10\"><select id=\"masterselector\" class=\"form-control\">\n\t\t\t\t\t\t{% for master in masterips %}\n\t\t\t\t\t\t<option value=\"{{master.split(\"@\")[0]}}\">{{master.split(\"@\")[1]}}</option>\n\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t    </select></div>\n\t\t\t\t    </div>\n\t\t\t\t\t<br/>\n\t\t\t\t\t<div class=\"form-group\"><label class=\"col-sm-2 control-label\"></label>\n\t\t\t\t\t    <div class=\"col-sm-10\"><p id=\"masterdesc\">{{masterdesc}}</p></div>\n\t\t\t\t    </div>\n\t\t\t\t\t<br/>\n\t\t\t\t    <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Image Choose</label>\n\t\t\t\t\t    <div class=\"col-sm-10\">\n\t\t\t\t\t    \t<table id=\"imagetable\" class=\"table table-striped table-bordered table-hover table-image\" >\n\t\t\t\t\t\t    <thead>\n\t\t\t\t\t\t    <tr>\n\t\t\t\t\t\t    \t<th>ImageName</th>\n\t\t\t\t\t\t\t<th>Type</th>\n\t\t\t\t\t\t\t<th>Owner</th>\n\t\t\t\t\t\t\t<th>Size</th>\n\t\t\t\t\t\t\t<th>Description</th>\n\t\t\t\t\t\t\t<th>Choose</th>\n\t\t\t\t\t\t    </tr>\n\t\t\t\t\t\t    </thead>\n\t\t\t\t\t\t    <tbody>\n\t\t\t\t\t\t    <tr>\n\t\t\t\t\t\t\t<td>base</td>\n\t\t\t\t\t\t\t<td>public</td>\n\t\t\t\t\t\t\t<td>docklet</td>\n\t\t\t\t\t\t\t<td>--</td>\n\t\t\t\t\t\t\t<td>A base image for you</td>\n\t\t\t\t\t\t\t<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"base_base_base\" checked=\"checked\" onchange=\"setMinDiskSize(0)\"></label></div></td>\n\t\t\t\t\t\t    </tr>\n\t\t\t\t\t\t    {% for image in images['private'] %}\n\t\t\t\t\t\t    <tr>\n\t\t\t\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t\t\t\t<td>private</td>\n\t\t\t\t\t\t\t<td>{{user}}</td>\n\t\t\t\t\t\t\t<td>{{image['size_format']}}</td>\n\t\t\t\t\t\t\t<td><a href=\"/image/{{masterips[0].split(\"@\")[0]}}/description/{{image['name']}}_{{user}}_private/\" target=\"_blank\">{{image['description']}}</a></td>\n\t\t\t\t\t\t\t<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"{{image['name']}}_{{user}}_private\" onchange=\"setMinDiskSize({{image['size_in_mb']}})\"></label></div></td>\n\t\t\t\t\t\t    </tr>\n\t\t\t\t\t\t    {% endfor %}\n\t\t\t\t\t\t    {% for p_user,p_images in images['public'].items() %}\n\t\t\t\t\t\t    \t{% for image in p_images %}\n\t\t\t\t\t\t    \t<tr>\n\t\t\t\t\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t\t\t\t\t<td>public</td>\n\t\t\t\t\t\t\t\t<td>{{p_user}}</td>\n\t\t\t\t\t\t\t\t<td>{{image['size_format']}}</td>\n\t\t\t\t\t\t\t\t<td><a href=\"/image/{{masterips[0].split(\"@\")[0]}}/description/{{image['name']}}_{{p_user}}_public/\" target=\"_blank\">{{image['description']}}</a></td>\n\t\t\t\t\t\t\t\t<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"{{image['name']}}_{{p_user}}_public\" onchange=\"setMinDiskSize({{image['size_in_mb']}})\"></label></div></td>\n\t\t\t\t\t\t    \t</tr>\n\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t    {% endfor %}\n\t\t\t\t\t\t    </tbody>\n\t\t\t\t\t        </table>\n\t\t\t\t\t    </div>\n                    </div>\n\n\t\t\t\t    <div class=\"hr-line-dashed\"></div>\n\t\t\t\t    <div class=\"panel-group\" id=\"accordion\">\n                        <div class=\"panel panel-default\">\n                            <div class=\"panel-heading\">\n                                <h4 class=\"panel-title\">\n\t\t\t\t\t<a data-toggle=\"collapse\" data-panel=\"#accordion\" href=\"#collapseOne\">\n                                        show advanced options\n                                    </a>\n                                </h4>\n                            </div>\n\t\t\t    <div id=\"collapseOne\" class=\"panel-collapse collapse\">\n                                <div class=\"panel-body\">\n\t\t\t\t                    <div class=\"form-group\">\n                                        <label class=\"col-sm-2 control-label\">CPU</label>\n                                        <div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"cpuSetting\" id=\"cpuSetting\" value = {{defaultsetting['cpu']}}  /> {{usage['cpu']}}CORE/{{quota['cpu']}}CORE\n                                        </div>\n                                    </div>\n\t\t\t\t                    <div class=\"form-group\">\n                                        <label class=\"col-sm-2 control-label\">MEMORY</label>\n                                        <div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"memorySetting\" id=\"memorySetting\" value = {{defaultsetting['memory']}}  /> {{usage['memory']}}MB/{{quota['memory']}}MB\n                                        </div>\n                                    </div>\n\t\t\t\t                    <div class=\"form-group\">\n                                        <label class=\"col-sm-2 control-label\">DISK</label>\n                                        <div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"diskSetting\" id=\"diskSetting\" value= {{defaultsetting['disk']}}  /> {{usage['disk']}} MB/{{quota['disk']}}MB (min value is the size of image + 100)\n                                        </div>\n                                    </div>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    <br/>\n\t\t\t\t    <div class=\"hr-line-dashed\"></div>\n\t\t\t\t    <div class=\"row\">\n\t\t\t\t    <div class=\"form-group\">\n\t\t\t\t\t    <div class=\"col-sm-4 col-sm-offset-2\">\n\t\t\t\t\t\t    <button class=\"btn btn-primary\" type=\"submit\">Create</button>\n\t\t\t\t\t    </div>\n\t\t\t\t    </div>\n\t\t\t\t    </div>\n                            </form>\n                        </div>\n                    </div>\n                    </div>\n\n</div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n    <!-- Custom and plugin javascript -->\n    <script src=\"https://cdn.bootcss.com/pace/1.0.2/pace.min.js\"></script>\n\n    <!-- Steps -->\n    <script src=\"https://cdn.bootcss.com/jquery-steps/1.1.0/jquery.steps.min.js\"></script>\n\n    <!-- Jquery Validate -->\n    <script src=\"https://cdn.bootcss.com/jquery-validate/1.15.0/jquery.validate.min.js\"></script>\n\n\n\t<script src=\"https://cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.js\"></script>\n\t<script src=\"https://cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n\t<script src=\"https://cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n\n\t<script type=\"text/javascript\">\n\t\tfunction setMinDiskSize(minSize) {\n\t\t\tvar disk = document.getElementById('diskSetting');\n\t\t\tdisk.setAttribute('min', minSize+100);\n\t\t}\n\t</script>\n\n    <script type=\"text/javascript\">\n\t\t$(\"select#masterselector\").change(function() {\n\t\t\tvar masterip=$(this).children('option:selected').val();\n\t\t\tvar mastername=$(this).children('option:selected').html();\n\t\t\tconsole.log(masterip);\n\t\t\tdocument.getElementById(\"form\").action=\"/workspace/\"+masterip+\"/add/\";\n\t\t\tvar host = window.location.host;\n\t\t\t$.post(\"//\"+host+\"/getmasterdesc/\"+mastername+\"/\", {}, function(data) {\n\t\t\t\t$(\"#masterdesc\").html(data);\n\t\t\t});\n\t\t\t$.post(\"//\"+host+\"/image/\"+masterip+\"/list/\",{},function(data){\n\t\t\t\tvar images = data.images;\n\t\t\t\tvar imagehtml =\n\t\t\t\t\t\"<thread>\"\n\t\t\t\t\t+\"<tr>\"\n\t\t\t\t\t+\"<th>ImageName</th>\"\n\t\t\t\t\t+\"<th>Type</th>\"\n\t\t\t\t\t+\"<th>Owner</th>\"\n\t\t\t\t\t+\"<th>Description</th>\"\n\t\t\t\t\t+\"<th>Choose</th>\"\n                                        +\"</tr>\"\n\t\t\t\t\t+\"</thead>\"\n\t\t\t\t\t+\"<tbody>\"\n\t\t\t\t\t+\"<tr>\"\n\t\t\t\t\t+\"<td>base</td>\"\n\t\t\t\t\t+\"<td>public</td>\"\n\t\t\t\t\t+\"<td>docklet</td>\"\n\t\t\t\t\t+\"<td>A base image for you</td>\"\n\t\t\t\t\t+'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"base_base_base\" checked=\"checked\"></label></div></td>'\n\t\t\t\t\t+\"</tr>\";\n\t\t\t\tfor(var index in images.private) {\n\t\t\t\t\tvar image = images.private[index];\n\t\t\t\t\timagehtml +=\n\t\t\t\t\t\t\"<tr>\"\n\t\t\t\t\t\t+\"<td>\"+image.name+\"</td>\"\n\t\t\t\t\t\t+\"<td>private</td>\"\n\t\t\t\t\t\t+\"<td>{{user}}</td>\"\n\t\t\t\t\t\t+'<td><a href=\"/image/' + masterip + '/description/' + image.name + '_' + '{{user}}' + '_private/\" target=\"_blank\">' + image.description + '</a></td>'\n\t\t\t\t\t\t+'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"' + image.name + '_{{user}}_private\"><label></div></td>'\n\t\t\t\t\t\t+\"</tr>\";\n\t\t\t\t}\n\t\t\t\tfor(var p_user in images.public) {\n\t\t\t\t\tfor(var index in images.public[p_user]) {\n\t\t\t\t\t\timage=images.public[p_user][index];\n\t\t\t\t\t\timagehtml +=\n\t\t\t\t\t\t\t\"<tr>\"\n\t\t\t\t\t\t\t+\"<td>\"+image.name+\"</td>\"\n\t\t\t\t\t\t\t+\"<td>public</td>\"\n\t\t\t\t\t\t\t+\"<td>\" + p_user + \"</td>\"\n\t\t\t\t\t\t\t+'<td><a href=\"/image/' + masterip + '/description/' + image.name + \"_\" + p_user + '_public/\" target=\"_blank\">' + image.description + '</a></td>'\n\t\t\t\t\t\t\t+'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image\" value=\"' + image.name + \"_\" + p_user + '_public\"><label></div></td>';\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t$(\"#imagetable\").html(imagehtml);\n\t\t},\"json\");\n\t});\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/base_AdminLTE.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>{% block title %}Docklet | Workspace{% endblock %}</title>\n  <!-- Tell the browser to be responsive to screen width -->\n  <meta content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" name=\"viewport\">\n  <link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\n\n  <link href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\n\n  <!-- Font Awesome -->\n  <link href=\"//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n\n  <!-- Ionicons -->\n  <link href=\"//cdn.bootcss.com/ionicons/2.0.1/css/ionicons.min.css\" rel=\"stylesheet\">\n\n  <link href=\"//cdn.bootcss.com/animate.css/3.5.1/animate.min.css\" rel=\"stylesheet\">\n  <link href=\"//cdn.bootcss.com/toastr.js/latest/css/toastr.min.css\" rel=\"stylesheet\">\n\n  <!-- Theme style -->\n\n  <link rel=\"stylesheet\" href=\"/static/dist/css/AdminLTE.min.css\">\n\n  <link rel=\"stylesheet\" href=\"/static/dist/css/skins/skin-blue.min.css\">\n\n\n  {%block css_src %}{% endblock %}\n\n\n</head>\n\n<body class=\"hold-transition skin-blue sidebar-mini\">\n<div class=\"wrapper\">\n\n  <!-- Main Header -->\n  <header class=\"main-header\">\n\n    <!-- Logo -->\n    <a href=\"\" class=\"logo\">\n      <!-- mini logo for sidebar mini 50x50 pixels -->\n      <span class=\"logo-mini\"></span>\n      <!-- logo for regular state and mobile devices -->\n      <span class=\"logo-lg\"><b>Docklet</b></span>\n    </a>\n\n    <!-- Header Navbar -->\n    <nav class=\"navbar navbar-static-top\" role=\"navigation\">\n      <!-- Sidebar toggle button-->\n      <a href=\"#\" class=\"sidebar-toggle\" data-toggle=\"offcanvas\" role=\"button\">\n        <span class=\"sr-only\">Toggle navigation</span>\n      </a>\n      <!-- Navbar Right Menu -->\n      <div class=\"navbar-custom-menu\">\n        <ul class=\"nav navbar-nav\">\n          <!-- Messages: style can be found in dropdown.less-->\n          <li class=\"dropdown notifications-menu\">\n          <a id='notificationIcon' href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\">\n            <i class=\"fa fa-bell-o\"></i>\n{#            <span class=\"label label-warning\">0</span>#}\n          </a>\n          <ul class=\"dropdown-menu\">\n            <li id=\"notificationHeader\" class=\"header\">You have 0 notifications</li>\n            <li>\n              <!-- inner menu: contains the actual data -->\n              <ul id='notificationList' class=\"menu\">\n{#                <li>#}\n{#                  <a href=\"#\">#}\n{#                    <i class=\"fa fa-envelop\"></i> Notification title#}\n{#                  </a>#}\n{#                </li>#}\n              </ul>\n            </li>\n            <li class=\"footer\"><a href=\"/notification/detail/all/\">View all</a></li>\n          </ul>\n          </li>\n          <li class=\"dropdown user user-menu\">\n            <!-- Menu Toggle Button -->\n            <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\">\n              <!-- The user image in the navbar-->\n              <img src=\"{{ mysession['avatar'] }}\" class=\"user-image\" alt=\"User Image\">\n              <!-- hidden-xs hides the username on small devices so only the image appears. -->\n              <span class=\"hidden-xs\">{{ mysession['nickname'] }}</span>\n            </a>\n            <ul class=\"dropdown-menu\">\n              <!-- The user image in the menu -->\n              <li class=\"user-header\">\n                <img src=\"{{ mysession['avatar'] }}\" class=\"img-circle\" alt=\"User Image\">\n\n                <p>\n                  {{ mysession['nickname'] }}\n                  <small>{{ mysession['description'] }}</small>\n                  <small>{{ beans }} <img src=\"/static/img/bean.png\" />\n                  <a href=\"/beans/application/\" class=\"btn btn-default btn-xs\">Apply</a></small>\n                </p>\n              </li>\n              <!-- Menu Body -->\n\n              <!-- Menu Footer-->\n              <li class=\"user-footer\" style=\"background-color:#e6e6e6\">\n                <div class=\"pull-left\">\n                  <a href=\"/user/info/\" class=\"btn btn-default btn-flat\">Profile</a>\n                </div>\n                <div class=\"pull-right\">\n                  <a href=\"/logout/\" class=\"btn btn-default btn-flat\">Sign out</a>\n                </div>\n              </li>\n            </ul>\n          </li>\n          <!-- Control Sidebar Toggle Button -->\n          <li class=\"dropdown user user-menu\">\n            <!-- Menu Toggle Button -->\n            <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\">\n\t\t    <strong>Bug Report</strong>\n            </a>\n            <ul class=\"dropdown-menu\">\n              <!-- The user image in the menu -->\n\t      <form action=\"/bug/report/\" method=\"POST\">\n          <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n\t\t<textarea id=\"bugmessage\" name=\"bugmessage\" style=\"width:250px; height:200px\"></textarea>\n              \t<li class=\"user-footer\" style=\"background-color:#e6e6e6\">\n              \t  <div class=\"pull-right\">\n              \t    <button type=\"submit\" class=\"btn btn-default btn-flat\">Submit</a>\n              \t  </div>\n              \t</li>\n\t      </form>\n            </ul>\n          </li>\n          <li>\n            <a href=\"/document/\" target=\"_blank\"><strong>Help</strong></a>\n          </li>\n          <li>\n            <a href=\"/logout/\" ><strong>Logout</strong></a>\n          </li>\n        </ul>\n      </div>\n    </nav>\n  </header>\n  <!-- Left side column. contains the logo and sidebar -->\n  <aside class=\"main-sidebar\">\n\n    <!-- sidebar: style can be found in sidebar.less -->\n    <section class=\"sidebar\">\n\n      <!-- Sidebar user panel (optional) -->\n      <div class=\"user-panel\">\n        <div class=\"pull-left image\">\n          <img src=\"{{ mysession['avatar'] }}\" class=\"img-circle\" alt=\"User Image\">\n        </div>\n        <div class=\"pull-left info\">\n          <p>{{ mysession['nickname'] }}</p>\n          <!-- Status -->\n          <a href=\"#\"><i class=\"fa fa-circle text-success\"></i> {{ mysession['status']}}</a>\n          <!-- Beans -->\n          <a href=\"/beans/application/\"><i><img src=\"/static/img/bean.png\" /></i> {{ beans }}</a>\n        </div>\n      </div>\n\n      <!-- Sidebar Menu -->\n      <ul class=\"sidebar-menu\">\n        <li class=\"header\">USER OPERATIONS</li>\n        <!-- Optionally, you can add icons to the links -->\n        <li class=\"active\" id=\"nav_Dashboard\">\n            <a href=\"/dashboard/\"><i class=\"fa fa-th-large\"></i> <span class=\"nav-label\">Workspace</span></a>\n        </li>\n        <li id=\"nav_Config\">\n            <a href=\"/config/\"><i class=\"fa fa-gears\"></i> <span class=\"nav-label\">Config</span></a>\n        </li>\n\n        <li id=\"nav_Status\">\n            <a href='/vclusters/'><i class=\"fa fa-bar-chart\"></i> <span class=\"nav-label\">Status</span></a>\n        </li>\n        <li id=\"nav_History\">\n            <a href='/history/'><i class=\"fa fa-history\"></i> <span class=\"nav-label\">History</span></a>\n        </li>\n        <li id=\"nav_Batch\">\n            <a href='/batch_jobs/'><i class=\"fa fa-tasks\"></i> <span class=\"nav-label\">Batch</span></a>\n        </li>\n\n\n  {% if mysession['usergroup'] == 'root' or mysession['usergroup'] == 'admin'%}\n        <li class=\"header\">ADMIN OPERATIONS</li>\n        <li id=\"nav_Hosts\">\n            <a href='/hosts/'><i class=\"fa fa-sitemap\"></i> <span class=\"nav-label\">Hosts</span></a>\n        </li>\n        <li id=\"user_List\">\n            <a href='/user/list/'><i class=\"fa fa-users\"></i> <span class=\"nav-label\">Users</span></a>\n        </li>\n        <li id=\"nav_admin_batch\">\n            <a href='/admin_batch_list'><i class=\"fa fa-tasks\"></i> <span class=\"nav-label\">Batch</span></a>\n        </li>\n        <li id=\"nav_Notification\">\n            <a href='/notification/'><i class=\"fa fa-envelope\"></i> <span class=\"nav-label\">Notifications</span></a>\n        </li>\n        <li id=\"settings\">\n\t    <a href='/settings/'><i class=\"fa fa-user\"></i> <span class=\"nav-label\">Settings</span></a>\n        </li>\n\n\t      <li id=\"nav_Cloud\">\n\t\t      <a href=\"/cloud/\"><i class=\"fa fa-cloud\"></i> <span class=\"nav-label\">Cloud</span></a>\n\t      <li>\n        <li id=\"logs\">\n            <a href='/logs/'><i class=\"fa fa-user\"></i> <span class=\"nav-label\">Logs</span></a>\n        </li>\n  {% endif %}\n\n      </ul>\n      <!-- /.sidebar-menu -->\n    </section>\n    <!-- /.sidebar -->\n  </aside>\n\n  <!-- Content Wrapper. Contains page content -->\n  <div class=\"content-wrapper\">\n    <!-- Content Header (Page header) -->\n    <section class=\"content-header\">\n      <h1>\n        <strong>{% block panel_title %}Workspace{% endblock %}</strong>\n      </h1>\n      {% block panel_list %}\n                  <ol class=\"breadcrumb\">\n                      <li>\n                          <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n                      </li>\n                      <li class=\"active\">\n                          <strong>Workspace</strong>\n                      </li>\n                  </ol>\n  \t\t{% endblock %}\n    </section>\n    <!-- Main content -->\n    <section class=\"content\">\n\n    {% block content %}\n    {% endblock %}\n\n    </section>\n    <!-- /.content -->\n  </div>\n  <!-- /.content-wrapper -->\n\n  <!-- Main Footer -->\n  <footer class=\"main-footer\">\n    <!-- To the right -->\n    <div class=\"pull-right hidden-xs\">\n\t\t    <i><a href=\"https://github.com/unias/docklet\">Docklet {{ version }}</a></i>\n    </div>\n    <!-- Default to the left -->\n    <strong>Copyright</strong>&copy;&nbsp;2019 <a href=\"https://unias.github.io/docklet\">UniAS</a>@<a href=\"http://www.sei.pku.edu.cn\"> SEI, PKU</a>\n\n  </footer>\n\n</div>\n<!-- ./wrapper -->\n\n<!-- REQUIRED JS SCRIPTS -->\n\n<!-- jQuery 2.2.1 -->\n<script src=\"//cdn.bootcss.com/jquery/2.2.1/jquery.min.js\"></script>\n<!-- Bootstrap 3.3.5 -->\n<script src=\"//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\n<!-- AdminLTE App -->\n<script src=\"/static/dist/js/app.min.js\"></script>\n\n<script src=\"//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js\"></script>\n<script src=\"//cdn.bootcss.com/jQuery-slimScroll/1.3.7/jquery.slimscroll.min.js\"></script>\n<script src=\"//cdn.bootcss.com/toastr.js/latest/js/toastr.min.js\"></script>\n\n\n\n<script type=\"text/javascript\">\n    var pathname = window.location.pathname;\n    pathname = pathname.split(/\\//);\n    if(pathname[1] != 'dashboard')\n        $(\"#nav_Dashboard\").removeClass(\"active\");\n    if(pathname[1] == 'vclusters')\n        $(\"#nav_Status\").addClass(\"active\");\n    else if(pathname[1] == 'hosts')\n        $(\"#nav_Hosts\").addClass(\"active\");\n    else if(pathname[1] == 'config')\n\t\t$(\"#nav_Config\").addClass(\"active\");\n    else if(pathname[1] == 'history')\n\t\t$(\"#nav_History\").addClass(\"active\");\n    else if(pathname[1] == 'user')\n    {\n  \t\t  if (pathname[2] == 'list')\n          $(\"#user_List\").addClass(\"active\");\n    }\n    else if(pathname[1] == 'notification')\n        $(\"#nav_Notification\").addClass('active');\n    else if(pathname[1] == 'admin')\n        $(\"#admin\").addClass('active');\n    else if(pathname[1] == 'settings')\n        $(\"#settings\").addClass('active');\n\n</script>\n\n<script type=\"text/javascript\">\n    $.ajaxSetup({\n      headers: {'X-CSRFToken':'{{ csrf_token() }}'},\n    });\n    var ajaxCfg = {\n        type : \"post\",\n        url : '/notification/query_self/',\n        dataType : \"json\",\n        data : {},\n        success : function(data) {\n            console.log(data);\n            var notifies = data.data;\n            var len = notifies.length;\n            var cnt = 0;\n\t\t\tvar unread = 0;\n\n            var now = new Date().getTime();\n            for(var t = 0; t < len; t++) {\n                var notify = notifies[t];\n                var createDate = notify.create_date.substring(0, 19);\n\t\t\t\tvar isRead = notify.isRead;\n                createDate = createDate.replace(/-/g,'/');\n                var from = new Date(createDate).getTime();\n                var delta = now - from;\n                if(delta <= 604800000) {\n                    cnt ++;\n                }\n\t\t\t\tif(isRead == 0){\n\t\t\t\t\tunread ++;\n\t\t\t\t}\n            }\n\n            $('#notificationIcon').find('span').remove();\n            $('#notificationList').empty();\n\n            if(unread != 0)\n                $(\"<span class=\\\"label label-warning\\\">\"+unread+\"</span>\").appendTo($('#notificationIcon'));\n\n            $(\"#notificationHeader\").html(\"You have \" + len + \" notifications, \"+cnt+\" in 1 week\");\n            for(var i = 0; i < len; i++) {\n                notify = notifies[i];\n                console.log(notify);\n\t\t\t\tvar isRead = notify.isRead;\n                var a = $(\"<a href=\\\"/notification/detail/\"+ notify.id +\"/\\\"><i class=\\\"fa fa-envelope\\\"></i> \"+ notify.title +\"</a>\")\n                var item = $(\"<li></li>\");\n                item.append(a);\n                $(\"#notificationList\").append(item);\n            }\n\n        },\n        error: function (xhr, type) {\n            console.log(xhr);\n        }\n    }\n\n    $(document).ready(function(){\n        $.ajax(ajaxCfg);\n        var refresh = setInterval(function(){\n            $.ajax(ajaxCfg);\n        }, 10000);\n    });\n</script>\n\n\n{% if mysession['status'] == 'init' %}\n<script type=\"text/javascript\">\n  $(document).ready(function() {\n    toastr.options = {\n      \"closeButton\": false,\n      \"debug\": true,\n      \"progressBar\": false,\n      \"preventDuplicates\": false,\n      \"positionClass\": \"toast-top-left\",\n      \"onclick\": function(){\n        window.location.href=\"/activate/\";\n      },\n      \"showDuration\": \"0\",\n      \"hideDuration\": \"0\",\n      \"timeOut\": \"0\",\n      \"extendedTimeOut\": \"0\",\n      \"showEasing\": \"swing\",\n      \"hideEasing\": \"linear\",\n      \"showMethod\": \"fadeIn\",\n      \"hideMethod\": \"fadeOut\"\n    };\n    toastr.error(\"You are not activated. Click this notification to activate your account.\");\n  });\n</script>\n\n{% endif %}\n\n{% if mysession['status'] == 'applying' %}\n<script type=\"text/javascript\">\n  $(document).ready(function() {\n    toastr.options = {\n      \"closeButton\": false,\n      \"debug\": true,\n      \"progressBar\": false,\n      \"preventDuplicates\": false,\n      \"positionClass\": \"toast-top-left\",\n      \"onclick\": function(){\n      },\n      \"showDuration\": \"0\",\n      \"hideDuration\": \"0\",\n      \"timeOut\": \"0\",\n      \"extendedTimeOut\": \"0\",\n      \"showEasing\": \"swing\",\n      \"hideEasing\": \"linear\",\n      \"showMethod\": \"fadeIn\",\n      \"hideMethod\": \"fadeOut\"\n    };\n    toastr.warning(\"You applying is being checked.\");\n  });\n</script>\n\n{% endif %}\n\n\n{% block script_src %}\n{% endblock %}\n</body>\n\n</html>\n"
  },
  {
    "path": "web/templates/batch/batch_admin_list.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n{% block title %}Docklet | Batch Job{% endblock %}\r\n\r\n{% block panel_title %}Batch Job{% endblock %}\r\n\r\n{% block css_src %}\r\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\r\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\r\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\r\n\r\n{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n  <li class=\"active\">\r\n      <strong>Batch Job</strong>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n{% block content %}\r\n<div class=\"row\">\r\n         <div class=\"col-lg-12\">\r\n           <div class=\"box box-info\">\r\n                <div class=\"box-header with-border\">\r\n                    <h3 class=\"box-title\">Batch Job List</h3>\r\n\r\n                  <div class=\"box-tools pull-right\">\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\r\n                    </button>\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\r\n                  </div>\r\n                </div>\r\n                 <div class=\"box-body\">\r\n\r\n                 {% for master in masterips %}\r\n                 {% for job_info in job_list[master.split('@')[0]] %}\r\n                 <div class=\"modal inmodal\" id='OutputModal_{{ master.split('@')[1] }}_{{ job_info['job_id'] }}' tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\r\n                   <div class=\"modal-dialog\">\r\n                     <div class=\"modal-content animated fadeIn\">\r\n                       <div class=\"modal-header\">\r\n                         <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\r\n                         <h4 class=\"modal-title\">Job:{{ job_info['job_name'] }}({{ job_info['job_id'] }}) Stdout and Stderr of tasks</h4>\r\n                       </div>\r\n                       <div class=\"modal-body\">\r\n                         <table width=\"100%\" cellspacing=\"0\" class=\"table table-bordered table-striped table-hover table-output\">\r\n                           <thead>\r\n                             <tr>\r\n                               <th>Task ID</th>\r\n                               <th>Vnode ID</th>\r\n                               <th>Stdout</th>\r\n                               <th>Stderr</th>\r\n                             </tr>\r\n                           </thead>\r\n                           <tbody>\r\n                             {% for taskid in job_info['tasks'] %}\r\n                             {% for vnodeid in range(job_info['tasks_vnodeCount'][taskid]) %}\r\n                             <tr>\r\n                               <td>{{ taskid }}</td>\r\n                               <td>{{ vnodeid }}</td>\r\n                               <td><a class=\"btn btn-info btn-xs\" href='/batch_job/output/{{ master.split('@')[0] }}/{{ job_info[\"job_id\"] }}/{{ taskid }}/{{ vnodeid }}/stdout/' target=\"_blank\">Stdout</a></td>\r\n                               <td><a class=\"btn btn-info btn-xs\" href='/batch_job/output/{{ master.split('@')[0] }}/{{ job_info[\"job_id\"] }}/{{ taskid }}/{{ vnodeid }}/stderr/' target=\"_blank\">Stderr</a></td>\r\n                             </tr>\r\n                             {% endfor %}\r\n                             {% endfor %}\r\n                           </tbody>\r\n                         </table>\r\n                         <div class=\"modal-footer\">\r\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\r\n                         </div>\r\n                       </div>\r\n                     </div>\r\n                   </div>\r\n                 </div>\r\n                 {% endfor %}\r\n                 {% endfor %}\r\n                 <div class=\"table\">\r\n                 <table width=\"100%\" cellspacing=\"0\" style=\"margin: 0 auto;\" class=\"table table-striped table-bordered table-hover table-batch\">\r\n                    <thead>\r\n                        <tr>\r\n                            <th>Location</th>\r\n                            <th>Username</th>\r\n                            <th>ID</th>\r\n                            <th>Name</th>\r\n                            <th>Status</th>\r\n                            <th>Operations</th>\r\n                            <th>Create Time</th>\r\n                            <th>End Time</th>\r\n                            <th>billing</th>\r\n                            <th>Stdout and Stderr</th>\r\n                            <th>Detailed Info</th>\r\n                        </tr>\r\n                    <thead>\r\n                    <tbody>\r\n                      {% for master in masterips %}\r\n                      {% for job_info in job_list[master.split('@')[0]] %}\r\n                        <tr>\r\n                            <td>{{ master.split('@')[1] }}</td>\r\n                            <td>{{ job_info['username'] }}</td>\r\n                            <td>{{ job_info['job_id'] }}</td>\r\n                            <td>{{ job_info['job_name'] }}</td>\r\n                            <td>\r\n                            {{ job_info['status'] }}\r\n                            </td>\r\n                            {% if job_info['status'] == 'done' or job_info['status'] == 'failed' or job_info['status'] == 'stopping' or job_info['status'] == 'stopped'%}\r\n                            <td><button type=\"button\"  class=\"btn btn-xs btn-default\"> &nbsp;Stop&nbsp;&nbsp; </button></td>\r\n                            {% else %}\r\n                            <td><a href=\"/admin_batch_job/{{master.split(\"@\")[0]}}/stop/{{ job_info['job_id'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-danger\"> &nbsp;Stop&nbsp; </button></a></td>\r\n                            {% endif %}\r\n                            <td>{{ job_info['create_time'] }}</td>\r\n                            <td>{{ job_info['end_time'] }}</td>\r\n                            <td>{{ job_info['billing'] }} <img src='/static/img/bean.png' /></td>\r\n                            <td><a role=\"button\" class=\"btn btn-info btn-xs\" id='{{ master }}_{{ job_info['job_id'] }}_output' data-toggle=\"modal\" data-target='#OutputModal_{{ master.split('@')[1] }}_{{ job_info['job_id'] }}'>Get Output</a></td>\r\n                            <td><a href=\"/batch_job/{{master.split(\"@\")[0]}}/info/{{ job_info['job_id'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-info\"> &nbsp;Info&nbsp; </button></a></td>\r\n                        </tr>\r\n                    {% endfor %}\r\n                    {% endfor %}\r\n                    </tbody>\r\n                 </table>\r\n                 </div>\r\n               </div>\r\n             </div>\r\n         </div>\r\n\t</div>\r\n\r\n{% endblock %}\r\n{% block script_src %}\r\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\r\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\r\n\r\n<script type=\"text/javascript\">\r\n$(document).ready(function() {\r\n   $(\".table-batch\").DataTable({\"scrollX\":true,\"order\":[[ 6, \"desc\" ]]});\r\n   $(\".table-output\").DataTable({\r\n   \"lengthChange\":false});\r\n});\r\nfunction sendAdd(){\r\n    document.getElementById(\"addForm\").submit();\r\n}\r\nfunction sendDel(){\r\n    document.getElementById(\"delForm\").submit();\r\n}\r\n</script>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/batch/batch_create.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Create Batch Job{% endblock %}\n\n{% block css_src %}\n<!--<style>\n.divcontent { overflow-y:scroll; height:200px;}\n</style>-->\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n{% block panel_title %}Batch Job Info{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n</ol>\n{% endblock %}\n\n<div>\n{% block content %}\n<div class=\"row\">\n                <div class=\"col-lg-12\">\n                  <div class=\"box box-info\">\n                       <div class=\"box-header with-border\">\n                         <h3 class=\"box-title\">Batch Job Create</h3>\n\n                         <div class=\"box-tools pull-right\">\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                           </button>\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                         </div>\n                       </div>\n                        <div class=\"box-body\">\n                <form id=\"form\" class=\"form-horizontal\" action=\"/batch_job/{{masterips[0].split(\"@\")[0]}}/add/\" method=\"POST\">\n\n                    <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                    <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Job Name</label>\n                        <div class=\"col-sm-10\"><input type=\"text\" class=\"form-control\" name=\"jobName\" id=\"job_name\" required></div>\n                    </div>\n                    <br/>\n                    <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Location</label>\n                    <div class=\"col-sm-10\"><select id=\"masterselector\" class=\"form-control\">\n                       {% for master in masterips %}\n                       <option value=\"{{master.split(\"@\")[0]}}\">{{master.split(\"@\")[1]}}</option>\n                       {% endfor %}\n                    </select></div>\n                    </div>\n                    <div class=\"hr-line-dashed\"></div>\n                    <br/>\n                        <div class=\"form-group\"><label class=\"col-sm-2 control-label\">Priority</label>\n                        <div class=\"col-sm-10\"><select id=\"priority_selector\" class=\"form-control\" name=\"jobPriority\">\n                        {% for priority in range(10) %}\n                        <option value=\"{{priority}}\">{{priority}}</option>\n                        {% endfor %}\n                        </select></div>\n                    </div>\n                    <br/>\n\n                    <div class=\"hr-line-dashed\"></div>\n                    <div class=\"panel-group\" id=\"accordion\">\n                    <!-- Tasks -->\n                    </div>\n                    <br/>\n                    <div class=\"hr-line-dashed\"></div>\n                    <div class=\"row\">\n                      <div class=\"form-group\">\n                        <div class=\"col-sm-4 col-sm-offset-2\">\n                            <button class=\"btn btn-primary\" type=\"button\" id=\"add_task\" class=\"btn btn-box-tool\" title=\"add a task\">Add Task <i class=\"fa fa-plus\"></i></button>\n                            <button class=\"btn btn-primary\" type=\"submit\">Create Job</button>\n                        </div>\n                      </div>\n                    </div>\n                            </form>\n                        </div>\n                    </div>\n                    </div>\n\n</div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n\n    <script src=\"//cdn.bootcss.com/pace/1.0.2/pace.min.js\"></script>\n\n    <!-- Steps -->\n    <script src=\"//cdn.bootcss.com/jquery-steps/1.1.0/jquery.steps.min.js\"></script>\n\n    <!-- Jquery Validate -->\n    <script src=\"//cdn.bootcss.com/jquery-validate/1.15.0/jquery.validate.min.js\"></script>\n\n\n    <script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n    <script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n    <script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n    <script src=\"//cdn.bootcss.com/jquery-validate/1.17.0/jquery.validate.js\"></script>\n\n    <script type=\"text/javascript\">\n    var task_number = 0;\n    var mapping_number = 0;\n    var images_text = \"{{ images }}\";\n    images_text = images_text.replace(/&#39;/g,\"\\\"\");\n    console.log('images_text', images_text);\n    var images_info = JSON.parse(images_text);\n    console.log('images_info', images_info);\n    var allmachines = \"{{ allmachines }}\";\n    allmachines = allmachines.replace(/&#39;/g,\"\\\"\");\n    console.log('allmachines', allmachines);\n    allmachines = JSON.parse(allmachines);\n    pending_gpu_tasks = \"{{ pending_gpu_tasks }}\";\n    pending_gpu_tasks = pending_gpu_tasks.replace(/&#39;/g,\"\\\"\");\n    console.log('allmachines', pending_gpu_tasks);\n    pending_gpu_tasks = JSON.parse(pending_gpu_tasks);\n    var user = \"{{ user }}\";\n    $().ready(function() {\n        $(\"#form\").validate();\n    });\n\n    function removeTask(obj) {\n        $(\"#task_pannel_\" + obj.id).remove();\n    }\n\n    function unfoldTask(obj){\n        $(\"#collapse\" + obj.id).collapse('hide');\n    }\n\n    function chmountPath(obj,task_num,mapping_num) {\n      cellid = 'mapping_mountpath_' + task_num + '_' + mapping_num;\n      $('#'+cellid).val(\"/root/oss/\"+obj.value);\n    }\n\n    function removeMapping(obj) {\n        $(\"#mapping_\" + obj.id).remove();\n    }\n\n    function addMapping(obj,task_num) {\n        mapping_number += 1;\n        var table = $(\"#storage_mapping_\" + obj.id)[0];\n        var new_mapping = table.insertRow();\n        new_mapping.id = \"mapping_\" + task_num + \"_\" + mapping_number;\n        var provider = new_mapping.insertCell();\n        var bucket_name = new_mapping.insertCell();\n        var accessKey = new_mapping.insertCell();\n        var secretKey = new_mapping.insertCell();\n        var endpoint = new_mapping.insertCell();\n        var mountpath = new_mapping.insertCell();\n        var remove = new_mapping.insertCell();\n        bucket_name.innerHTML = '<input type=\"text\" class=\"form-control\" name=\"mappingBucketName_' + task_num + '_' + mapping_number + '\" id=\"mapping_bucketname_'\n                             + task_num + '_' + mapping_number + '\" onKeyUp=\"chmountPath(this,'+task_num+','+mapping_number+');\" required/>';\n        accessKey.innerHTML = '<input type=\"text\" class=\"form-control\" name=\"mappingAccessKey_' + task_num + '_' + mapping_number + '\" id=\"mapping_accessKey_'\n                             + task_num + '_' + mapping_number + '\" required/>';\n        secretKey.innerHTML = '<input type=\"text\" class=\"form-control\" name=\"mappingSecretKey_' + task_num + '_' + mapping_number + '\" id=\"mapping_secretKey_'\n                             + task_num + '_' + mapping_number + '\" required/>';\n        endpoint.innerHTML = 'http://<input type=\"text\" class=\"form-control\" name=\"mappingEndpoint_' + task_num + '_' + mapping_number + '\" id=\"mapping_endpoint_'\n                             + task_num + '_' + mapping_number + '\" required/>';\n        mountpath.innerHTML = '<input type=\"text\" class=\"form-control\" name=\"mappingMountpath_' + task_num + '_' + mapping_number + '\" id=\"mapping_mountpath_'\n                             + task_num + '_' + mapping_number + '\" readonly=\"true\" required/>';\n        provider.innerHTML = '<select class=\"form-control\" name=\"mappingProvider_' + task_num + '_' + mapping_number + '\" id=\"mapping_provider_'\n                          + task_num + '_' + mapping_number + '\">'\n                          +'<option>Aliyun</option></select>';\n        remove.innerHTML = '<div class=\"box-tool pull-left\"><button type=\"button\" id=\"' + task_num + '_' + mapping_number +'\" onclick=\"removeMapping(this)\" class=\"btn btn-xs btn-danger\">'\n                          +'Remove</button></div>';\n    }\n\n    $(\"select#masterselector\").change(function() {\n      var masterip=$(this).children('option:selected').val();\n      $(\"#form\").attr(\"action\",\"/batch_job/\"+ masterip +\"/add/\");\n      var mastername=$(this).children('option:selected').html();\n      console.log(masterip);\n      var host = window.location.host;\n        var images = images_info;\n        for(var tnum = 1; tnum<=task_number; ++tnum)\n        {\n        var imagehtml =\n          \"<thead>\"\n          +\"<tr>\"\n          +\"<th>ImageName</th>\"\n          +\"<th>Type</th>\"\n          +\"<th>Owner</th>\"\n          +\"<th>Size</th>\"\n          +\"<th>Description</th>\"\n          +\"<th>Choose</th>\"\n                                        +\"</tr>\"\n          +\"</thead>\"\n          +\"<tbody>\"\n          +\"<tr>\"\n          +\"<td>base</td>\"\n          +\"<td>public</td>\"\n          +\"<td>docklet</td>\"\n          +\"<td>--</td>\"\n          +\"<td>A base image for you</td>\"\n          +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + tnum + '\" value=\"base_base_base\" checked=\"checked\"></label></div></td>'\n          +\"</tr>\";\n        for(var index in images[masterip].private) {\n          var image = images[masterip].private[index];\n          imagehtml +=\n            \"<tr>\"\n            +\"<td>\"+image.name+\"</td>\"\n            +\"<td>private</td>\"\n            +\"<td>\" + user + \"</td>\"\n            +\"<td>\"+image.size_format+\"</td>\"\n            +'<td><a href=\"/image/' + masterip + '/description/' + image.name + '_' + user + '_private/\" target=\"_blank\">' + image.description + '</a></td>'\n            +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + tnum + '\" value=\"'+image.name+'_' + user + '_private\"><label></div></td>'\n            +\"</tr>\";\n        }\n        for(var p_user in images[masterip].public) {\n          for(var index in images[masterip].public[p_user]) {\n            image=images[masterip].public[p_user][index];\n            imagehtml +=\n              \"<tr>\"\n              +\"<td>\"+image.name+\"</td>\"\n              +\"<td>public</td>\"\n              +\"<td>\" + p_user + \"</td>\"\n              +\"<td>\"+image.size_format+\"</td>\"\n              +'<td><a href=\"/image/' + masterip + '/description/' + image.name + \"_\" + p_user + '_public/\" target=\"_blank\">' + image.description + '</a></td>'\n              +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + tnum + '\" value=\"'+image.name+'_' + p_user + '_public\"><label></div></td>'\n              +\"</tr>\";\n          }\n        }\n        imagehtml += \"</tbody>\";\n        $(\"#imagetable\"+tnum).html(imagehtml);\n        }\n    });\n\n    function addTask() {\n      task_number += 1;\n      var masterip=$(\"select#masterselector\").children('option:selected').val();\n      //mapping_number = 0;\n      var task_html = '';\n      var images = images_info\n      task_html +=\n            '<div class=\"panel panel-default\" id=\"task_pannel_' + task_number + '\">'\n           +'<div class=\"panel-heading\">'\n           +'<h4 class=\"panel-title\">'\n           +'<a data-toggle=\"collapse\" data-panel=\"#accordion\" href=\"#collapse' + task_number + '\">'\n           +'Task #' + task_number\n           +'</a><div class=\"box-tools pull-right\"><button type=\"button\" id=\"' + task_number + '\" onclick=\"removeTask(this)\" class=\"btn btn-box-tool\"><i class=\"fa fa-times\"></i></button></div>'\n           +'</h4></div>'\n           +'<div id=\"collapse' + task_number + '\" class=\"panel-collapse collapse in\">'\n           +'<div class=\"panel-body\">'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">Running Path</label>'\n           +'<div class=\"col-sm-3\"><input type=\"text\" class=\"form-control\" name=\"srcAddr_' + task_number + '\" id=\"srcAddr_' + task_number + '\" value=\"/root\" required/>'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Command</label>'\n           +'<div class=\"col-sm-3\"><input type=\"text\" class=\"form-control\" name=\"command_' + task_number + '\" id=\"command_' + task_number + '\"  required/>'\n           +'</div></div>'\n\n     task_html +=\n            '<div class=\"form-group\"><label class=\"col-sm-2 control-label\">Image Choose</label>'\n           +'<div class=\"col-sm-10\">'\n           +'<table id=\"imagetable' + task_number +'\" class=\"table table-striped table-bordered table-hover table-image\" >'\n           +\"<thead>\"\n           +\"<tr>\"\n           +\"<th>ImageName</th>\"\n           +\"<th>Type</th>\"\n           +\"<th>Owner</th>\"\n           +\"<th>Size</th>\"\n           +\"<th>Description</th>\"\n           +\"<th>Choose</th>\"\n           +\"</tr>\"\n           +\"</thead>\"\n           +\"<tbody>\"\n           +\"<tr>\"\n           +\"<td>base</td>\"\n           +\"<td>public</td>\"\n           +\"<td>docklet</td>\"\n           +\"<td>--</td>\"\n           +\"<td>A base image for you</td>\"\n           +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + task_number + '\" value=\"base_base_base\" checked=\"checked\"></label></div></td>'\n           +\"</tr>\";\n           for(var index in images[masterip].private) {\n             var image = images[masterip].private[index];\n              task_html +=\n                \"<tr>\"\n               +\"<td>\"+image.name+\"</td>\"\n               +\"<td>private</td>\"\n               +\"<td>\" + user + \"</td>\"\n               +\"<td>\"+image.size_format+\"</td>\"\n               +'<td><a href=\"/image/' + masterip + '/description/' + image.name + '_' + user + '_private/\" target=\"_blank\">' + image.description + '</a></td>'\n               +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + task_number + '\" value=\"'+image.name+'_' + user + '_private\"><label></div></td>'\n               +\"</tr>\";\n           }\n           for(var p_user in images[masterip].public) {\n             for(var index in images[masterip].public[p_user]) {\n               image=images[masterip].public[p_user][index];\n               task_html +=\n                 \"<tr>\"\n                 +\"<td>\"+image.name+\"</td>\"\n                 +\"<td>public</td>\"\n                 +\"<td>\" + p_user + \"</td>\"\n                 +\"<td>\"+image.size_format+\"</td>\"\n                 +'<td><a href=\"/image/' + masterip + '/description/' + image.name + \"_\" + p_user + '_public/\" target=\"_blank\">' + image.description + '</a></td>'\n                 +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"image_' + task_number + '\" value=\"'+image.name+'_' + p_user + '_public\"><label></div></td>'\n                 +\"</tr>\";\n              }\n           }\n    task_html +=\n            '</tbody></table>'\n           +'</div>'\n           +'</div>'\n    task_html +=\n            '<div class=\"panel panel-default\" id=\"task_subpannel_' + task_number + '\">'\n           +'<div class=\"panel-heading\">'\n           +'<h4 class=\"panel-title\">'\n           +'<a data-toggle=\"collapse\" data-panel=\"#acc\" href=\"#subcollapse' + task_number + '\">'\n           +'Show detailed options'\n           +'</a>'\n           +'</h4></div>'\n           +'<div id=\"subcollapse' + task_number + '\" class=\"panel-collapse collapse\">'\n           +'<div class=\"panel-body\">'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">CPU</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"cpuSetting_' + task_number + '\" id=\"cpuSetting_' + task_number + '\" value = 1  min=\"1\" max=\"8\" required/>'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Memory</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"memorySetting_' + task_number + '\" id=\"memorySetting_' + task_number + '\" value = 1024  min=\"100\" max=\"8196\" required/>'\n           +'</div>MB</div>'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">GPU</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"gpuSetting_' + task_number + '\" id=\"gpuSetting_' + task_number + '\" value= 0  min=\"0\" max=\"2\" required/>'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Disk</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"diskSetting_' + task_number + '\"  id=\"diskSetting_' + task_number + '\" value= 1024 min=\"128\" max=\"10000\" required/>'\n           +'</div>MB</div>'\n\n    task_html +=\n            '<div class=\"form-group\"><label class=\"col-sm-2 control-label\">GPU Preference</label>'\n           +'<div class=\"col-sm-10\">'\n           +'<table id=\"gputable' + task_number +'\" class=\"table table-striped table-bordered table-hover table-image\" >'\n           +\"<thead>\"\n           +\"<tr>\"\n           +\"<th>GPU Name</th>\"\n           +\"<th>Memory</th>\"\n           +\"<th>Price/h</th>\"\n           +\"<th>Pending Tasks</th>\"\n           +\"<th>Choose</th>\"\n           +\"</tr>\"\n           +\"</thead>\"\n           +\"<tbody>\"\n           +\"<tr>\"\n           +\"<td>No Preference</td>\"\n           +\"<td>--</td>\"\n           +\"<td>--</td>\"\n           +\"<td>\" + pending_gpu_tasks[masterip]['null'] + \"</td>\"\n           +'<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"gpuPreference_' + task_number + '\" value=\"null\" checked=\"checked\"></label></div></td>'\n           +\"</tr>\";\n           for(var index in allmachines[masterip]) {\n              var gpuinfo = allmachines[masterip][index]\n              if (gpuinfo.length > 0) {\n                task_html +=\n                  \"<tr>\"\n                 +\"<td>\"+gpuinfo[0]['name']+\"</td>\"\n                 +\"<td>\"+gpuinfo[0]['memory_max']+\"</td>\"\n                 +\"<td>\"+gpuinfo[0]['price']+\" <img src='/static/img/bean.png' /></td>\";\n                if (pending_gpu_tasks[masterip][gpuinfo[0]['name']]) {\n                  task_html += \"<td>\" + pending_gpu_tasks[masterip][gpuinfo[0]['name']] + \"</td>\"\n                } else {\n                  task_html += \"<td>0</td>\"\n                }\n                task_html +=\n                  '<td><div class=\"i-checks\"><label><input type=\"radio\" name=\"gpuPreference_' + task_number + '\" value=\"'+gpuinfo[0]['name']+'\"><label></div></td>'\n                 +\"</tr>\";\n               }\n           }\n    task_html +=\n            '</tbody></table>'\n           +'</div>'\n           +'</div>'\n\n\n    task_html +=\n            '<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">VNode Number</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"vnodeCount_' + task_number + '\" id=\"vnodeCount_' + task_number + '\" value= 1  min=\"1\" max=\"14\" required/>'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Max Retry Times</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"retryCount_' + task_number + '\" id=\"retryCount_' + task_number + '\" value= 1  min=\"0\" max=\"5\" required/>'\n           +'</div></div>'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">Dependency&nbsp<i class=\"fa fa-question-circle\" title=\"The tasks ids that this task depends on, seperate them with commas, eg: 1, 2\"></i></label>'\n           +'<div class=\"col-sm-3\"><input type=\"text\" class=\"form-control\" name=\"dependency_' + task_number + '\" id=\"dependency_' + task_number + '\"  />'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Expire Time</label>'\n           +'<div class=\"col-sm-3\"><input type=\"number\" class=\"form-control\" name=\"expTime_' + task_number + '\" id=\"expTime_' + task_number + '\" value= 3600  min=\"10\" max=\"86400\" required/>'\n           +'</div>Seconds</div>'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">Stderr Redirect Path</label>'\n           +'<div class=\"col-sm-3\"><input type=\"text\" class=\"form-control\" placeholder=\"/path/to/file or /path/\" name=\"stdErrRedPth_' + task_number + '\" id=\"stdErrRedPth_' + task_number + '\" value=\"/root/nfs/batch_{jobid}/\"  required/>'\n           +'</div>'\n           +'<label class=\"col-sm-2 control-label\">Stdout Redirect Path</label>'\n           +'<div class=\"col-sm-3\"><input type=\"text\" class=\"form-control\" placeholder=\"/path/to/file or /path/\" name=\"stdOutRedPth_' + task_number + '\" id=\"stdOutRedPth_' + task_number + '\" value=\"/root/nfs/batch_{jobid}/\" required/>'\n           +'</div></div>'\n           +'<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">Run on: </label>'\n           +'<div class=\"col-sm-3\"><input type=\"radio\" name=\"runon_' + task_number + '\" value=\"all\"  checked=\"checked\"/>All vnodes &nbsp'\n           +' <input type=\"radio\" name=\"runon_' + task_number + '\" value=\"master\"  />One vnode(master)</div>'\n           +'<label class=\"col-sm-2 control-label\">Start at the Same Time</label>'\n           +'<div class=\"col-sm-3\"><input type=\"checkbox\" name=\"atSameTime_' + task_number + '\" checked=\"checked\"/>'\n           +'</div></div>'\n\n      task_html +=\n           '<div class=\"form-group\">'\n           +'<label class=\"col-sm-2 control-label\">Object Storage Mapping<br/>'\n           +'<button type=\"button\" id=\"' + task_number + '\" class=\"btn btn-primary btn-xs\" title=\"add an external storage mapping\" onclick=\"addMapping(this,'+task_number+')\">'\n           +'Add<i class=\"fa fa-plus\"></i></button></label>'\n           +'<div class=\"col-sm-10\"><table class=\"table table-bordered\" id=\"storage_mapping_' + task_number + '\">'\n           +'<thead>'\n           +'<tr><th>Provider</th><th>Bucket Name</th><th>AccessKey ID</th><th>AccessKey Secret</th><th>Endpoint</th><th>Mount Path</th><th>Remove</th></tr>'\n           +'</thead>'\n           +'<tbody>'\n           +'</tbody>'\n           +'</table></div>'\n           +'</div>'\n           +'</div></div></div>'\n           +'<div class=\"box-tools pull-right\"><button type=\"button\" id=\"' + task_number + '\" onclick=\"unfoldTask(this)\" class=\"btn btn-primary\">Confirm</button></div>'\n           +'</div></div>'\n      $(task_html).appendTo(\"#accordion\");\n    }\n    addTask();\n    $(\"#add_task\").click(addTask);\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/batch/batch_info.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Batch Job Info{% endblock %}\n\n{% block panel_title %}Info for {{ jobinfo['job_id'] }}{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/batch_jobs/'>Batch Job</a>\n  </li>\n  <li class='active'>\n      <strong>Info</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block content %}\n<div class=\"row\">\n<div class=\"col-md-12\">\n  <div class=\"box box-info\">\n       <div class=\"box-header with-border\">\n         <h3 class=\"box-title\">Overview</h3>\n\n         <div class=\"box-tools pull-right\">\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n           </button>\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n         </div>\n       </div>\n              <div class=\"box-body table-responsive\">\n                  <table class=\"table table-bordered\">\n                      <thead>\n                      <tr>\n                        <th>Job ID</th>\n                        <th>Name</th>\n                        <th>Priority</th>\n                        <th>Status</th>\n                        <th>Create Time</th>\n                        <th>End Time</th>\n                        <th>Billing</th>\n                      </tr>\n                      </thead>\n                      <tbody>\n                      <tr>\n                          <td>{{ jobinfo['job_id'] }}</td>\n                          <td>{{ jobinfo['job_name'] }}</td>\n                          <td>{{ jobinfo['priority'] }}</td>\n                          <td>{{ jobinfo['status'] }}</td>\n                          <td>{{ jobinfo['create_time'] }}</td>\n                          <td>{{ jobinfo['end_time'] }}</td>\n                          <td>{{ jobinfo['billing'] }} <img src='/static/img/bean.png' /></td>\n                      </tr>\n                      </tbody>\n                  </table>\n\n              </div>\n          </div>\n      </div>\n</div>\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n  <div class=\"box box-info\">\n       <div class=\"box-header with-border\">\n         <h3 class=\"box-title\">Tasks Overview</h3>\n\n         <div class=\"box-tools pull-right\">\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n           </button>\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n         </div>\n       </div>\n              <div class=\"box-body table-responsive\">\n                  <table width=\"100%\" cellspacing=\"0\" style=\"margin: 0 auto;\" id=\"table-tasks\" class=\"table table-striped table-bordered table-hover\">\n                      <thead>\n                      <tr>\n                        <th>Task Index</th>\n                        <th>Status</th>\n                        <th>Failed Reason(if fails)</th>\n                        <th>Tried Times</th>\n                        <th>Start Time</th>\n                        <th>End Time</th>\n                        <th>Total Running Time</th>\n                        <th>Billing</th>\n                      </tr>\n                      </thead>\n                      <tbody>\n                      {% for task in jobinfo['tasks'] %}\n                      <tr>\n                          <td>{{ task['idx'] }}</td>\n                          {% if task['status'] == 'scheduling' %}\n                          <td>{{ task['status'] }}({{ task['order'] }} scheduling tasks before)</td>\n                          {% else %}\n                          <td>{{ task['status'] }}</td>\n                          {% endif %}\n                          <td>{{ task['failed_reason'] }}</td>\n                          <td>{{ task['tried_times'] }}</td>\n                          <td>{{ task['start_time'] }}</td>\n                          <td>{{ task['end_time'] }}</td>\n                          <td>{{ task['running_time'] }} s</td>\n                          <td>{{ task['billing'] }} <img src='/static/img/bean.png' /></td>\n                      </tr>\n                      {% endfor %}\n                      </tbody>\n                  </table>\n\n              </div>\n          </div>\n      </div>\n</div>\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n  <div class=\"box box-info\">\n       <div class=\"box-header with-border\">\n         <h3 class=\"box-title\">Tasks Configs</h3>\n\n         <div class=\"box-tools pull-right\">\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n           </button>\n           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n         </div>\n       </div>\n              <div class=\"box-body\">\n                  {% for task in jobinfo['tasks'] %}\n                  <div class=\"panel panel-default\" id=\"task_pannel_{{ task['idx'] }}\">\n                    <div class=\"panel-heading\">\n                      <h4 class=\"panel-title\">\n                        <a data-toggle=\"collapse\" data-panel=\"#accordion\" href=\"#collapse{{ task['idx'] }}\">\n                          Task #{{ task['idx'] }}\n                        </a>\n                      </h4>\n                    </div>\n                    <div id=\"collapse{{ task['idx'] }}\" class=\"panel-collapse collapse in\">\n                      <div class=\"panel-body\">\n                        <div class=\"table-responsive\">\n                            <table class=\"table table-bordered table-hover\">\n                                <thead>\n                                <tr>\n                                  <th>CPU Cores</th>\n                                  <th>Memory</th>\n                                  <th>GPU</th>\n                                  <th>Disk</th>\n                                  <th>VNode Number</th>\n                                  <th>Max Retry Times</th>\n                                </tr>\n                                </thead>\n                                <tbody>\n                                <tr>\n                                    <td>{{ task['config']['cpuSetting'] }}</td>\n                                    <td>{{ task['config']['memorySetting'] }} MB</td>\n                                    <td>{{ task['config']['gpuSetting'] }}</td>\n                                    <td>{{ task['config']['diskSetting'] }} MB</td>\n                                    <td>{{ task['config']['vnodeCount'] }}</td>\n                                    <td>{{ task['config']['retryCount'] }}</td>\n                                </tr>\n                                </tbody>\n                                <thead>\n                                <tr>\n                                  <th>Running Path</th>\n                                  <th>Expire Time</th>\n                                  <th>Stdout Redirect Path</th>\n                                  <th>Stderr Redirect Path</th>\n                                  <th>Dependency</th>\n                                  <th>Command</th>\n                                </tr>\n                                </thead>\n                                <tbody>\n                                <tr>\n                                    <td>{{ task['config']['srcAddr'] }}</td>\n                                    <td>{{ task['config']['expTime'] }} seconds</td>\n                                    <td>{{ task['config']['stdOutRedPth'] }}</td>\n                                    <td>{{ task['config']['stdErrRedPth'] }}</td>\n                                    <td>{{ task['config']['dependency'] }}</td>\n                                    <td>{{ task['config']['command'] }}</td>\n                                </tr>\n                                </tbody>\n                                <thead>\n                                <tr>\n                                  <th>Run on</th>\n                                  <th>Start at the Same Time</th>\n                                  <th>Image Name</th>\n                                  <th>Image Owner</th>\n                                  <th>Image Type</th>\n                                </tr>\n                                </thead>\n                                <tbody>\n                                <tr>\n                                    {% if task['config']['runon'] == 'all' %}\n                                    <td>all vnodes</td>\n                                    {% else %}\n                                    <td>master vnode</td>\n                                    {% endif %}\n                                    {% if 'atSameTime' in task['config'].keys() %}\n                                    <td>True</td>\n                                    {% else %}\n                                    <td>False</td>\n                                    {% endif %}\n                                    {% if task['config']['image'] == 'base_base_base' %}\n                                    <td>base</td>\n                                    <td>docklet</td>\n                                    <td>public</td>\n                                    {% else %}\n                                    <td>{{ task['config']['image'].split('_')[0] }}</td>\n                                    <td>{{ task['config']['image'].split('_')[1] }}</td>\n                                    <td>{{ task['config']['image'].split('_')[2] }}</td>\n                                    {% endif %}\n                                </tr>\n                                </tbody>\n                            </table>\n                        </div>\n                        {% if 'mapping' in task['config'].keys() %}\n                        <div class=\"table-responsive\">\n                            <table class=\"table table-bordered table-hover\">\n                                <thead>\n                                  <tr>\n                                    <th>Provider</th>\n                                    <th>Bucket Name</th>\n                                    <th>AccessKey ID</th>\n                                    <th>Endpoint</th>\n                                    <th>Mount Path</th>\n                                  </tr>\n                                </thead>\n                                <tbody>\n                                  {% for key in task['config']['mapping'].keys() %}\n                                  <tr>\n                                    <td>{{ task['config']['mapping'][key]['mappingProvider'] }}</td>\n                                    <td>{{ task['config']['mapping'][key]['mappingBucketName'] }}</td>\n                                    <td>{{ task['config']['mapping'][key]['mappingAccessKey'] }}</td>\n                                    <td>{{ task['config']['mapping'][key]['mappingEndpoint'] }}</td>\n                                    <td>{{ task['config']['mapping'][key]['mappingMountpath'] }}</td>\n                                  </tr>\n                                  {% endfor %}\n                                </tbody>\n                            </table>\n                          </div>\n                          {% endif %}\n                      </div>\n                    </div>\n                  </div>\n                  {% endfor %}\n              </div>\n          </div>\n      </div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n\n<script type=\"text/javascript\">\n$(document).ready(function() {\n   $(\"#table-tasks\").DataTable({\"scrollX\":true,\"order\":[[ 0, \"asc\" ]]});\n});\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/batch/batch_list.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n{% block title %}Docklet | Batch Job{% endblock %}\r\n\r\n{% block panel_title %}Batch Job{% endblock %}\r\n\r\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n  <li class=\"active\">\r\n      <strong>Batch Job</strong>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n{% block content %}\r\n<div class=\"row\">\r\n         <div class=\"col-lg-12\">\r\n           <div class=\"box box-info\">\r\n                <div class=\"box-header with-border\">\r\n                    <h3 class=\"box-title\">Batch Job List</h3>\r\n\r\n                  <div class=\"box-tools pull-right\">\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\r\n                    </button>\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\r\n                  </div>\r\n                </div>\r\n                 <div class=\"box-body\">\r\n\r\n\t\t\t\t\t <p>\r\n\t\t\t\t\t <a href=\"/batch_job/create/\"><button type=\"button\" class=\"btn btn-primary btn-sm\"><i class=\"fa fa-plus\"></i> Create Batch Job</button></a>\r\n\t\t\t\t\t </p>\r\n                 {% for master in masterips %}\r\n                 {% for job_info in job_list[master.split('@')[0]] %}\r\n                 <div class=\"modal inmodal\" id='OutputModal_{{ master.split('@')[1] }}_{{ job_info['job_id'] }}' tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\r\n                   <div class=\"modal-dialog\">\r\n                     <div class=\"modal-content animated fadeIn\">\r\n                       <div class=\"modal-header\">\r\n                         <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\r\n                         <h4 class=\"modal-title\">Job:{{ job_info['job_name'] }}({{ job_info['job_id'] }}) Stdout and Stderr of tasks</h4>\r\n                       </div>\r\n                       <div class=\"modal-body\">\r\n                         <table width=\"100%\" cellspacing=\"0\" class=\"table table-bordered table-striped table-hover table-output\">\r\n                           <thead>\r\n                             <tr>\r\n                               <th>Task ID</th>\r\n                               <th>Vnode ID</th>\r\n                               <th>Stdout</th>\r\n                               <th>Stderr</th>\r\n                             </tr>\r\n                           </thead>\r\n                           <tbody>\r\n                             {% for taskid in job_info['tasks'] %}\r\n                             {% for vnodeid in range(job_info['tasks_vnodeCount'][taskid]) %}\r\n                             <tr>\r\n                               <td>{{ taskid }}</td>\r\n                               <td>{{ vnodeid }}</td>\r\n                               <td><a class=\"btn btn-info btn-xs\" href='/batch_job/output/{{ master.split('@')[0] }}/{{ job_info[\"job_id\"] }}/{{ taskid }}/{{ vnodeid }}/stdout/' target=\"_blank\">Stdout</a></td>\r\n                               <td><a class=\"btn btn-info btn-xs\" href='/batch_job/output/{{ master.split('@')[0] }}/{{ job_info[\"job_id\"] }}/{{ taskid }}/{{ vnodeid }}/stderr/' target=\"_blank\">Stderr</a></td>\r\n                             </tr>\r\n                             {% endfor %}\r\n                             {% endfor %}\r\n                           </tbody>\r\n                         </table>\r\n                         <div class=\"modal-footer\">\r\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\r\n                         </div>\r\n                       </div>\r\n                     </div>\r\n                   </div>\r\n                 </div>\r\n                 {% endfor %}\r\n                 {% endfor %}\r\n                 <div class=\"table\">\r\n                 <table width=\"100%\" cellspacing=\"0\" style=\"margin: 0 auto;\" class=\"table table-striped table-bordered table-hover table-batch\">\r\n                    <thead>\r\n                        <tr>\r\n                            <th>Location</th>\r\n                            <th>ID</th>\r\n                            <th>Name</th>\r\n                            <th>Status</th>\r\n                            <th>Operations</th>\r\n                            <th>Create Time</th>\r\n                            <th>End Time</th>\r\n                            <th>billing</th>\r\n                            <th>Stdout and Stderr</th>\r\n                            <th>Detailed Info</th>\r\n                        </tr>\r\n                    <thead>\r\n                    <tbody>\r\n                      {% for master in masterips %}\r\n                      {% for job_info in job_list[master.split('@')[0]] %}\r\n                        <tr>\r\n                            <td>{{ master.split('@')[1] }}</td>\r\n                            <td>{{ job_info['job_id'] }}</td>\r\n                            <td>{{ job_info['job_name'] }}</td>\r\n                            <td>\r\n                            {{ job_info['status'] }}\r\n                            </td>\r\n                            {% if job_info['status'] == 'done' or job_info['status'] == 'failed' or job_info['status'] == 'stopping' or job_info['status'] == 'stopped'%}\r\n                            <td><button type=\"button\"  class=\"btn btn-xs btn-default\"> &nbsp;Stop&nbsp;&nbsp; </button></td>\r\n                            {% else %}\r\n                            <td><a href=\"/batch_job/{{master.split(\"@\")[0]}}/stop/{{ job_info['job_id'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-danger\"> &nbsp;Stop&nbsp; </button></a></td>\r\n                            {% endif %}\r\n                            <td>{{ job_info['create_time'] }}</td>\r\n                            <td>{{ job_info['end_time'] }}</td>\r\n                            <td>{{ job_info['billing'] }} <img src='/static/img/bean.png' /></td>\r\n                            <td><a role=\"button\" class=\"btn btn-info btn-xs\" id='{{ master }}_{{ job_info['job_id'] }}_output' data-toggle=\"modal\" data-target='#OutputModal_{{ master.split('@')[1] }}_{{ job_info['job_id'] }}'>Get Output</a></td>\r\n                            <td><a href=\"/batch_job/{{master.split(\"@\")[0]}}/info/{{ job_info['job_id'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-info\"> &nbsp;Info&nbsp; </button></a></td>\r\n                        </tr>\r\n                    {% endfor %}\r\n                    {% endfor %}\r\n                    </tbody>\r\n                 </table>\r\n                 </div>\r\n               </div>\r\n             </div>\r\n         </div>\r\n\t</div>\r\n\r\n{% endblock %}\r\n{% block script_src %}\r\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\r\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\r\n\r\n<script type=\"text/javascript\">\r\n$(document).ready(function() {\r\n   $(\".table-batch\").DataTable({\"scrollX\":true,\"order\":[[ 5, \"desc\" ]]});\r\n   $(\".table-output\").DataTable({\r\n   \"lengthChange\":false});\r\n});\r\nfunction sendAdd(){\r\n    document.getElementById(\"addForm\").submit();\r\n}\r\nfunction sendDel(){\r\n    document.getElementById(\"delForm\").submit();\r\n}\r\n</script>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/batch/batch_output.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>Docklet | Batch {{ issue }}: {{ jobid }}/{{ taskid }}/{{ vnodeid }}</title>\n  <!-- Tell the browser to be responsive to screen width -->\n  <meta content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" name=\"viewport\">\n  <link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\n\n  <link href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\n\n  <!-- Font Awesome -->\n  <link href=\"//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n\n  <!-- Ionicons -->\n  <link href=\"//cdn.bootcss.com/ionicons/2.0.1/css/ionicons.min.css\" rel=\"stylesheet\">\n\n  <link href=\"//cdn.bootcss.com/animate.css/3.5.1/animate.min.css\" rel=\"stylesheet\">\n  <link href=\"//cdn.bootcss.com/toastr.js/latest/css/toastr.min.css\" rel=\"stylesheet\">\n\n  <!-- Theme style -->\n\n  <link rel=\"stylesheet\" href=\"/static/dist/css/AdminLTE.min.css\">\n\n  <link rel=\"stylesheet\" href=\"/static/dist/css/skins/skin-blue.min.css\">\n</head>\n\n<body>\n  <h3>Jobid: {{ jobid }}</h3>\n  <h3>Taskid: {{ taskid }}</h3>\n  <h3>VNodeid: {{ vnodeid }}</h3>\n  <h4><small>The output of {{ issue }} will be updated in every 2 seconds.</small></h4>\n  <hr>\n  <pre id=\"output\">{{ output }}</pre>\n  <!-- jQuery 2.2.1 -->\n  <script src=\"//cdn.bootcss.com/jquery/2.2.1/jquery.min.js\"></script>\n  <!-- Bootstrap 3.3.5 -->\n  <script src=\"//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\n  <!-- AdminLTE App -->\n  <script src=\"/static/dist/js/app.min.js\"></script>\n\n  <script src=\"//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js\"></script>\n  <script src=\"//cdn.bootcss.com/jQuery-slimScroll/1.3.7/jquery.slimscroll.min.js\"></script>\n  <script src=\"//cdn.bootcss.com/toastr.js/latest/js/toastr.min.js\"></script>\n\n  <script type=\"text/javascript\">\n    $.ajaxSetup({\n      headers: {'X-CSRFToken':'{{ csrf_token() }}'},\n    });\n    function updateOutput()\n    {\n      var host = window.location.host;\n      url = \"//\" + host + \"/batch/job/output/\" + \"{{ masterip }}\" + \"/\" + \"{{ jobid }}\" + \"/\" + \"{{ taskid }}\" + \"/\" + \"{{ vnodeid }}\" + \"/\" + \"{{ issue }}\" + \"/\";\n      $.post(url,{},function(data){\n        $(\"#output\").text(String(data.data));\n      },\"json\");\n    }\n    setInterval(updateOutput,2000);\n  </script>\n</body>\n</html>\n"
  },
  {
    "path": "web/templates/beansapplication.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Beans Application{% endblock %}\n\n{% block panel_title %}Beans Application{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Beans Application</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div class=\"row\">\n<div class=\"col-md-12\">\n<div class=\"box box-info\">\n    <div class=\"box-header with-border\">\n       <h3 class=\"box-title\">All Applications</h3>\n       <div class=\"box-tools pull-right\">\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n         </button>\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n       </div>\n     </div>\n       <div class=\"box-body\">\n       <p>\n       <button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#AddApplication\"><i class=\"fa fa-plus\"></i> Apply For Beans</button>\n       </p>\n\t<div class=\"modal inmodal\" id=\"AddApplication\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n             <div class=\"modal-dialog\">\n             <div class=\"modal-content animated fadeIn\">\n                     <div class=\"modal-header\">\n                         <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                         <i class=\"fa fa-plus modal-icon\"></i>\n                         <h4 class=\"modal-title\">Apply for more beans.</h4>\n                         <small class=\"font-bold\">Application Requirements: <br/>1.Your beans are less than 1000.<br/> 2.No application is on \"processing\".</small>\n                     </div>\n                     <div class=\"modal-body\">\n                       <form action=\"/beans/apply/\" method=\"POST\" id=\"beansapplyForm\">\n                          <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                          <div class=\"form-group\">\n                            <label>Number</label><small class=\"font-bold\"> How many beans do you need?</small>\n                            <input type=\"number\" class=\"form-control\" placeholder=\"100-5000\" name=\"number\" id=\"number\" min=\"100\" max=\"5000\" required />\n                          </div>\n                          <div class=\"form-group\">\n                            <label>Reason</label>\n                            <textarea class=\"form-control\" name=\"reason\" cols=\"28\" rows=\"5\" onKeyDown=\"textCounter(reason,remLen,300);\" onKeyUp=\"textCounter(reason,remLen,300);\" placeholder=\"Please describe what you will do in your workspace with the beans.\"></textarea>\n\t\t\t     You could only input <input name=\"remLen\" type=\"text\" value=\"300\" size=\"3\" readonly=\"readonly\"> more characters.\n                          </div>\n\n                     <div class=\"modal-footer\">\n                         <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                         <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                     </div>\n                       </form>\n                     </div>\n             </div>\n             </div>\n           </div>\n                   <div class=\"table table-responsive\">\n                     <table class=\"table table-striped table-bordered table-hover table-image\" >\n                        <thead>\n                    \t<tr>\n                        <th>Application ID</th>\n                        <th>Username</th>\n                        <th>Number</th>\n                        <th>Submission Time</th>\n                        <th>Reason</th>\n                        <th>Status</th>\n                    \t</tr>\n                    \t</thead>\n                    <tbody>\n                      {% for application in applications %}\n                      <tr>\n                      <td>{{ application.id }}</td>\n                      <td>{{ application.username }}</td>\n                      <td>{{ application.number }} <img src='/static/img/bean.png'></td>\n                      <td>{{ application.time }}</td>\n                      <td>{{ application.reason }}</td>\n                      <td>{{ application.status }}</td>\n                      </tr>\n                      {% endfor %}\n                    </tbody>\n\t\t  </table>\n                </div>\n\t  </div>\n        </div>\n</div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n<script src=\"https://cdn.bootcss.com/jquery-validate/1.17.0/jquery.validate.js\"></script>\n\n<script>\n         $(document).ready(function() {\n            $(\".table-image\").DataTable();\n         });\n         $().ready(function() {\n             $(\"#beansapplyForm\").validate();\n         });\nfunction textCounter(field,countfield,maxlimit)\n{\n   if(field.value.length > maxlimit)\n       field.value = field.value.substring(0,maxlimit);\n   else\n       countfield.value = maxlimit - field.value.length;\n}\n\n </script>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/cloud.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\n{% block title %}Docklet | Cloud{% endblock %}\n\n{% block panel_title %}Cloud{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Cloud</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n\n{% block content %}\n<ul class=\"nav nav-tabs\" role=\"tablist\" id=\"myTabs\">\n{% for master,info in settings.items() %}\n{% if loop.index == 1 %}\n<li role=\"presentation\" class=\"active\"><a href=\"#{{master.split(\"@\")[1]}}\" data-toggle=\"tab\" aria-controls=\"{{master.split(\"@\")[1]}}\">{{master.split(\"@\")[1]}}</a></li>\n{% else %}\n <li role=\"presentation\"><a href=\"#{{master.split(\"@\")[1]}}\" data-toggle=\"tab\" aria-controls=\"{{master.split(\"@\")[1]}}\">{{master.split(\"@\")[1]}}</a></li>\n{% endif %}\n{% endfor %}\n</ul>\n<div id=\"myTabContent\" class=\"tab-content\">\n{% for master,info in settings.items() %}\n{% if loop.index == 1 %}\n<div role=\"tabpanel\" class=\"tab-pane active\" aria-labelledby=\"{{master.split(\"@\")[1]}}\" id=\"{{master.split(\"@\")[1]}}\">\n{% else %}\n<div role=\"tabpanel\" class=\"tab-pane\" aria-labelledby=\"{{master.split(\"@\")[1]}}\" id=\"{{master.split(\"@\")[1]}}\">\n{% endif %}\n<div class=\"row\">\n <div class=\"col-md-12\">\n     <div class=\"box box-info\">\n         <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">Cloud Setting</h3>\n\n             <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i>\n                 </button>\n             </div>\n         </div>\n         <div class=\"box-body table-responsive\">\n\t\t <form action=\"/cloud/{{master.split(\"@\")[0]}}/setting/modify/\" method=\"POST\">\n       <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n\t\t\t <textarea id=\"setting\" name=\"setting\" class=\"form-control\" rows=\"20\">{{ info['result'] }}</textarea>\n\t\t\t <button type=\"submit\" class=\"btn btn-primary\">Save</button>\n\t\t </form>\n       \t </div>\n       </div>\n     </div>\n</div>\n</div>\n{% endfor %}\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"https://cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/config.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\n\n<!--\n\tConfig Page :\n\t\t1. images\n\t\t2. workspace templates\n\n-->\n\n{% block title %}Docklet | Config{% endblock %}\n\n{% block panel_title %}Config{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Config</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n\n{% block content %}\n{% for master in allclusters %}\n{% for clustername, clusterinfo in allclusters[master].items() %}\n<div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info collapsed-box\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">WorkSpace Name: {{ clustername }} <strong>@ {{master.split(\"@\")[1]}}</strong></h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-plus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n              <div class=\"box-body\" style=\"display:none\">\n\t\t\t\t<div class=\"row\">\n\t\t\t\t\t<div class=\"col-md-12\">\n            <div class=\"box box-info\">\n                 <div class=\"box-header with-border\">\n                   <h4 class=\"box-title\">VCLUSTER</h4>\n                   <h5>create_time:{{clusterinfo['create_time']}}&nbsp&nbsp&nbsp&nbsp&nbsp&nbspstart_time:{{clusterinfo['start_time']}}</h5>\n\n                   <div class=\"box-tools pull-right\">\n                     <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                     </button>\n                     <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                   </div>\n                 </div>\n\t\t\t\t\t\t<div class=\"box-body\">\n\t\t\t\t\t\t\t<p>\n\t\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#Scaleout_{{ clustername }}_{{master.split(\"@\")[1]}}\"><i class=\"fa fa-plus\"></i>Add Node</button>\n\t\t\t     \t\t\t\t</p>\n\t\t\t\t\t\t\t<div class=\"modal inmodal\" id=\"Scaleout_{{ clustername }}_{{master.split(\"@\")[1]}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                   <div class=\"modal-dialog\">\n                                   <div class=\"modal-content animated fadeIn\">\n                                           <div class=\"modal-header\">\n                                               <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                               <i class=\"fa fa-plus modal-icon\"></i>\n                                               <h4 class=\"modal-title\">Choose Image</h4>\n                                               <small class=\"font-bold\">Choose an image to add node</small>\n                                           </div>\n                                           <div class=\"modal-body\">\n                                                <div class=\"form-group\">\n\t\t\t\t\t\t\t<form action=\"/workspace/{{master.split(\"@\")[0]}}/scaleout/{{ clustername }}/\" method=\"POST\" >\n                   <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                <table class=\"table table-striped table-bordered table-hover table-image\">\n\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<th>ImageName</th>\n\t\t\t\t\t\t\t\t\t\t<th>Type</th>\n\t\t\t\t\t\t\t\t\t\t<th>Owner</th>\n                    <th>Size</th>\n\t\t\t\t\t\t\t\t\t\t<th>Choose</th>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td>base</td>\n\t\t\t\t\t\t\t\t\t\t<td>public</td>\n\t\t\t\t\t\t\t\t\t\t<td>docklet</td>\n                    <td>--</td>\n\t\t\t\t\t\t\t\t\t\t<td><input type=\"radio\" name=\"image\" value=\"base_base_base\" checked=\"checked\"></td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t{% for image in allimages[master]['private'] %}\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t\t\t\t\t\t\t<td>private</td>\n\t\t\t\t\t\t\t\t\t\t<td>{{mysession['username']}}</td>\n                    <td>{{image['size_format']}}</td>\n\t\t\t\t\t\t\t\t\t\t<td><input type=\"radio\" name=\"image\" value=\"{{image['name']}}_{{mysession['username']}}_private\"></td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t\t\t{% for p_user, p_images in allimages[master]['public'].items() %}\n\t\t\t\t\t\t\t\t\t\t{% for image in p_images %}\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>public</td>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{p_user}}</td>\n                      <td>{{image['size_format']}}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td><input type=\"radio\" name=\"image\" value=\"{{image['name']}}_{{p_user}}_public\"></td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n \t                    \t<div class=\"hr-line-dashed\"></div>\n\t\t\t\t<div class=\"panel-group\" id=\"accordion_{{clustername}}_{{master.split(\"@\")[1]}}\">\n                                     <div class=\"panel panel-default\">\n                                         <div class=\"panel-heading\">\n                                             <h4 class=\"panel-title\">\n\t\t\t\t\t\t     <a data-toggle=\"collapse\" data-panel=\"#accordion_{{clustername}}_{{master.split(\"@\")[1]}}\" href=\"#collapseOne_{{clustername}}_{{master.split(\"@\")[1]}}\">\n                                                     show advanced options\n                                                 </a>\n                                             </h4>\n                                         </div>\n\t\t\t\t\t <div id=\"collapseOne_{{clustername}}_{{master.split(\"@\")[1]}}\" class=\"panel-collapse collapse\">\n                                             <div class=\"panel-body\">\n\t\t\t\t                                 <div class=\"form-group\">\n                                                     <label class=\"control-label\">CPU</label>\n                                                     <div ><input type=\"number\" class=\"form-control\" name=\"cpuSetting\" id=\"cpuSetting\" value = {{defaultsetting['cpu']}}  /> {{usage['cpu']}}CORE/{{quota['cpu']}}CORE\n                                                     </div>\n                                                 </div>\n\t\t\t\t                                 <div class=\"form-group\">\n                                                     <label class=\"control-label\">MEMORY</label>\n                                                     <div ><input type=\"number\" class=\"form-control\" name=\"memorySetting\" id=\"memorySetting\" value = {{defaultsetting['memory']}}  /> {{usage['memory']}}MB/{{quota['memory']}}MB\n                                                     </div>\n                                                 </div>\n\t\t\t\t                                 <div class=\"form-group\">\n                                                     <label class=\"control-label\">DISK</label>\n                                                     <div><input type=\"number\" class=\"form-control\" name=\"diskSetting\" id=\"diskSetting\" value= {{defaultsetting['disk']}}  /> {{usage['disk']}} MB/{{quota['disk']}}MB (min value is the size of image + 100)\n                                                     </div>\n                                                 </div>\n                                             </div>\n                                         </div>\n                                     </div>\n                                 </div>\n\t\t\t\t\t\t<div class=\"modal-footer\">\n                                               \t\t<button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                               \t\t<button type=\"submit\" class=\"btn btn-success\">Add</button>\n                                                  </div>\n                                                  </form>\n                                                </div>\n\t\t\t\t\t   </div>\n\t\t\t\t   </div>\n\t\t\t\t   </div>\n\t\t\t     </div>\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>Node ID</th>\n\t\t\t\t <th>Node Name</th>\n                                 <th>IP Address</th>\n                                 <th>Status</th>\n\t\t\t\t <th>Image</th>\n\t\t\t\t <th>Save</th>\n\t\t\t\t <th>Delete</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for container in clusterinfo['containers'] %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ container['containername'] }}</td>\n                                 <td>{{ container['ip'] }}</td>\n\n                                 {% if  clusterinfo['status']  == 'stopped' %}\n\t\t\t\t\t\t\t\t <td><div class=\"text-warning\"><i class=\"fa fa-stop\"></i> Stopped</div></td>\n                                 {% else %}\n\t\t\t\t\t\t\t\t <td><div class=\"text-success\"><i class=\"fa fa-play\"></i> Running</div></td>\n                                 {% endif %}\n\n\t\t\t\t <td>{{ container['image'] }}</td>\n\t\t\t\t <td><button type=\"button\" class=\"btn btn-success btn-xs\" data-toggle=\"modal\" data-target=\"#DelModal_{{ container['containername'] }}_{{master.split(\"@\")[1]}}\">Save</button></td>\n                {% if container['containername'][-2:] == '-0' %}\n                <td><button class=\"btn btn-xs btn-default\">Delete</button></td>\n                {% else %}\n\t\t<td><a class=\"btn btn-xs btn-danger\" href=\"/workspace/{{master.split(\"@\")[0]}}/scalein/{{ clustername }}/{{ container['containername'] }}/\">Delete</a></td>\n                {% endif %}\n\t\t<div class=\"modal inmodal\" id=\"DelModal_{{ container['containername'] }}_{{master.split(\"@\")[1]}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                   <div class=\"modal-dialog\">\n                                   <div class=\"modal-content animated fadeIn\">\n                                           <div class=\"modal-header\">\n                                               <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                               <i class=\"fa fa-save modal-icon\"></i>\n                                               <h4 class=\"modal-title\">Save Image</h4>\n                                               <small class=\"font-bold\">Save Your Environment As a Image</small>\n                                           </div>\n                                           <div class=\"modal-body\">\n                                                <div class=\"form-group\">\n\t\t\t\t\t\t\t<form action=\"/workspace/{{master.split(\"@\")[0]}}/save/{{ clustername }}/{{ container['containername'] }}/\" method=\"POST\" id=\"saveImage\">\n              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                                  <label>Image Name</label>\n\t\t\t\t\t\t  <input type=\"text\" placeholder=\"Enter Image Name\" class=\"form-control\" name=\"ImageName\" id=\"ImageName\"/>\n\t\t\t\t\t\t  <br/>\n\t\t\t\t\t\t  <label>Description</label>\n\t\t\t\t\t\t  <textarea  rows=\"5\" cols=\"60\" name=\"description\" id=\"description\">please input your description</textarea>\n\t\t\t\t\t\t  <div class=\"modal-footer\">\n                                               \t\t<button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                               \t\t<button type=\"submit\" class=\"btn btn-success\">Save</button>\n                                                  </div>\n                                                  </form>\n                                                </div>\n                                           </div>\n                                       </div>\n                                   </div>\n                               </div>\n\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n            <div class=\"row\">\n    \t\t\t\t\t<div class=\"col-md-12\">\n                <div class=\"box box-info\">\n                     <div class=\"box-header with-border\">\n                       <h4 class=\"box-title\">TCP Ports Mapping</h4>\n                       <div class=\"box-tools pull-right\">\n                         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                         </button>\n                         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                       </div>\n                     </div>\n    \t\t\t\t\t\t<div class=\"box-body\">\n    \t\t\t\t\t\t\t<p>\n    \t\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#Addportsmapping_{{ clustername }}_{{master.split(\"@\")[1]}}\"><i class=\"fa fa-plus\"></i>Apply</button>\n    \t\t\t     \t\t\t\t</p>\n    \t\t\t\t\t\t\t<div class=\"modal inmodal\" id=\"Addportsmapping_{{ clustername }}_{{master.split(\"@\")[1]}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                       <div class=\"modal-dialog\">\n                                       <div class=\"modal-content animated fadeIn\">\n                                               <div class=\"modal-header\">\n                                                   <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                                   <i class=\"fa fa-plus modal-icon\"></i>\n                                                   <h4 class=\"modal-title\">Apply for TCP Ports Mapping</h4>\n                                                   <small class=\"font-bold\">Add a TCP port mapping for a node</small>\n                                               </div>\n                                               <div class=\"modal-body\">\n                                                      <form action=\"/port_mapping/add/{{master.split(\"@\")[0]}}/\" method=\"POST\" id=\"AddportsmappingForm\">\n                                                        <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                                        <div class=\"form-group\">\n                                                          <label>Cluster Name</label>\n                                                          <input type = \"text\" value=\"{{ clustername }}\" class=\"form-control\" name=\"clustername\" readonly=\"readonly\">\n                                                        </div>\n                                                         <div class=\"form-group\">\n                                                           <label>Node Name</label>\n                                                           <select class=\"form-control\" name=\"node_name\" onchange=\"chnodeip(this.value,node_ip)\">\n                                                            {% for container in clusterinfo['containers'] %}\n                                                             <option value=\"{{ container['containername'] }}\">{{ container['containername'] }}</option>\n                                                             {% endfor %}\n                                                           </select>\n                                                         </div>\n                                                         <div class=\"form-group\">\n                                                           <label>Node IP</label>\n                                                           <input type = \"text\" value=\"{{ clusterinfo[\"containers\"][0][\"ip\"][:clusterinfo[\"containers\"][0][\"ip\"].index(\"/\")] }}\" class=\"form-control\" name=\"node_ip\" readonly=\"readonly\">\n                                                         </div>\n                                                         <div class=\"form-group\">\n                                                           <label>Node Port</label><small class=\"font-bold\"> The port that the host port is mapping to(1-65535).</small>\n                                                           <input type=\"number\" class=\"form-control\" placeholder=\"1-65535\" value=\"80\" name=\"node_port\" id=\"node_port\" min=\"1\" max=\"65535\"/>\n                                                         </div>\n                                                    <div class=\"modal-footer\">\n                                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                                        <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                                                    </div>\n                                                      </form>\n                                                    </div>\n                                                  </div>\n                                                </div>\n                                              </div>\n                             <table class=\"table table-bordered\">\n                                 <thead>\n                                 <tr>\n                                     <th>Node Name</th>\n                                     <th>Node IP</th>\n                                     <th>Node Port</th>\n                                     <th>Host Public IP</th>\n                                     <th>Host Port</th>\n                                     <th>Delete</th>\n                                 </tr>\n                                 </thead>\n                                 <tbody>\n                                 {% for record in clusterinfo['port_mapping'] %}\n                                 <tr>\n                                     <td>{{ record['node_name'] }}</td>\n                                     <td>{{ record['node_ip'] }}</td>\n                                     <td>{{ record['node_port'] }}</td>\n                                     <td>{{ clusterinfo['proxy_public_ip'] }}</td>\n                                     <td>{{ record['host_port'] }}</td>\n                                     <td><a class=\"btn btn-xs btn-danger\" href=\"/port_mapping/delete/{{master.split(\"@\")[0]}}/{{ clustername }}/{{ record['node_name'] }}/{{ record['node_port'] }}/\">Delete</a></td>\n                                 {% endfor %}\n                                 </tbody>\n                             </table>\n    \t\t\t\t\t\t</div>\n    \t\t\t\t\t\t</div>\n    \t\t\t\t\t</div>\n  \t\t                </div>\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n{% endfor %}\n{% endfor %}\n<div class=\"row\">\n         <div class=\"col-lg-12\">\n           <div class=\"box box-info collapsed-box\">\n                <div class=\"box-header with-border\">\n                  <h3 class=\"box-title\">Image Info</h3>\n                  <div class=\"box-tools pull-right\">\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-plus\"></i>\n                    </button>\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                  </div>\n                </div>\n                 <div class=\"box-body\" style=\"display:none\">\n\n                     <table class=\"table table-striped table-bordered table-hover table-image\" >\n                         <thead>\n                         <tr>\n                             <th>ImageName</th>\n                             <th>Type</th>\n\t\t\t     <th>Owner</th>\n\t\t\t     <th>CreateTime</th>\n           <th>Size</th>\n\t\t\t     <th>Description</th>\n\t\t\t     <th>Location</th>\n\t\t\t     <th>Status</th>\n\t\t\t     <th>Operation</th>\n                         </tr>\n                         </thead>\n\t\t\t <tbody>\n\t\t\t    <tr>\n\t\t\t\t<td>base</td>\n\t\t\t\t<td>public</td>\n\t\t\t\t<td>docklet</td>\n\t\t\t\t<td>2015-01-01 00:00:00</td>\n        <td>--</td>\n\t\t\t\t<td>A Base Image For You</td>\n\t\t\t\t<td>--</td>\n\t\t\t\t<td></td>\n\t\t\t\t<td></td>\n\t\t\t    </tr>\n\t\t\t{% for master in allimages %}\n\t\t\t{% for image in allimages[master]['private'] %}\n\t\t\t <tr>\n\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t<td>private</td>\n\t\t\t\t<td>{{mysession['username']}}</td>\n\t\t\t\t<td>{{image['time']}}</td>\n        <td>{{image['size_format']}}</td>\n\t\t\t\t<td><a href=\"/image/{{master.split(\"@\")[0]}}/description/{{image['name']}}_{{mysession['username']}}_private/\" target=\"_blank\">{{image['description']}}</a></td>\n\t\t\t\t<td>{{master.split(\"@\")[1]}}</td>\n\t\t\t\t{% if image['isshared'] == 'false' %}\n\t\t\t\t\t<td>unshared</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<a href=\"/image/{{master.split(\"@\")[0]}}/share/{{ image['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-success\">share</button></a>\n\t\t\t\t\t\t<a href=\"/image/{{master.split(\"@\")[0]}}/delete/{{ image['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-danger\">delete</button></a>\n    \t\t\t\t\t\t<button type=\"button\" class=\"btn btn-xs btn-primary\" data-toggle=\"modal\" data-target=\"#Copyimage_{{ image['name'] }}_{{master.split(\"@\")[1]}}\">copy</button>\n    \t\t\t\t\t<div class=\"modal inmodal\" id=\"Copyimage_{{ image['name'] }}_{{master.split(\"@\")[1]}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n\t                                       <div class=\"modal-dialog\">\n        \t                               <div class=\"modal-content animated fadeIn\">\n                                               <div class=\"modal-header\">\n                                                   <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                                   <i class=\"fa fa-plus modal-icon\"></i>\n                                                   <h4 class=\"modal-title\">Copy image to other location</h4>\n                                               </div>\n                                               <div class=\"modal-body\">\n\t\t\t\t\t\t       <form action=\"/image/{{master.split(\"@\")[0]}}/copy/{{image['name']}}/\" method=\"POST\" id=\"CopyImageForm\">\n                    <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                \t\t\t\t<table class=\"table table-striped table-bordered table-hover table-image\">\n\t\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<th>Location</th>\n\t\t\t\t\t\t\t\t\t\t\t<th>choose</th>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t\t<tbody>\n\t\t\t\t\t\t\t\t\t\t{% for targetmaster in masterips %}\n\t\t\t\t\t\t\t\t\t\t{% if not master==targetmaster %}\n\t\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t\t<td>{{ targetmaster.split(\"@\")[1]}}</td>\n\t\t\t\t\t\t\t\t\t\t\t<td><input type=\"radio\" name=\"target\" value=\"{{targetmaster.split(\"@\")[0]}}\"></td>\n\t\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t<div class=\"modal-footer\">\n                                                        \t<button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                                        \t<button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                                                    \t</div>\n                                                      </form>\n                                                    </div>\n                                                  </div>\n                                                </div>\n\t\t\t\t\t</div>\n\t\t\t\t\t</td>\n\t\t\t\t{% else %}\n\t\t\t\t\t<td>shared</td>\n\t\t\t\t\t<td>\n\t\t\t\t\t\t<a href=\"/image/{{master.split(\"@\")[0]}}/unshare/{{ image['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-warning\">unshare</button></a>\n\t\t\t\t\t\t<a href=\"/image/{{master.split(\"@\")[0]}}/delete/{{ image['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-danger\">delete</button></a>\n    \t\t\t\t\t\t<button type=\"button\" class=\"btn btn-xs btn-primary\" data-toggle=\"modal\" data-target=\"#Copyimage_{{ image['name'] }}_{{master.split(\"@\")[1]}}\">Copy</button>\n\t\t\t\t\t</td>\n\t\t\t\t{% endif %}\n              \t\t </tr>\n\t\t        {% endfor %}\n\t       \t        {% for p_user,p_images in allimages[master]['public'].items() %}\n\t\t\t    \t{% for image in p_images %}\n\t\t\t    \t<tr>\n\t\t\t\t<td>{{image['name']}}</td>\n\t\t\t\t<td>public</td>\n\t\t\t\t<td>{{p_user}}</td>\n\t\t\t\t<td>{{image['time']}}</td>\n        <td>{{image['size_format']}}</td>\n\t\t\t\t<td><a href=\"/image/{{master.split(\"@\")[0]}}/description/{{image['name']}}_{{p_user}}_public/\" target=\"_blank\">{{image['description']}}</a></td>\n\t\t\t\t<td>{{master.split(\"@\")[1]}}</td>\n\t\t\t\t<td></td>\n\t\t\t\t{% if p_user == mysession['username'] %}\n\t\t\t\t<td><a href=\"/image/{{master.split(\"@\")[0]}}/unshare/{{ image['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-warning\">unshare</button></a></td>\n\t\t\t\t{% else %}\n\t\t\t\t<td></td>\n\t\t\t\t{% endif %}\n\t\t\t    \t</tr>\n\t\t\t\t{% endfor %}\n\t\t\t{% endfor %}\n\t\t\t{% endfor %}\n\t\t\t</tbody>\n\t\t  </table>\n\t\t</div>\n\t</div>\n\t</div>\n</div>\n\n{% endblock %}\n\n{% block script_src %}\n\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n\n<script>\n         $(document).ready(function() {\n            $(\".table-image\").DataTable();\n            $(\".table-image\").attr(\"style\",\"\");\n         });\n         var map_node_ip = [];\n         {% for master in allclusters %}\n         {% for clustername, clusterinfo in allclusters[master].items() %}\n         {% for container in clusterinfo['containers'] %}\n         map_node_ip[\"{{ container['containername'] }}\"] = \"{{ container[\"ip\"][:container[\"ip\"].index(\"/\")] }}\";\n         {% endfor %}\n         {% endfor %}\n         {% endfor %}\n         function chnodeip(node_name,field)\n         {\n           field.value = map_node_ip[node_name];\n         }\n </script>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/create_notification.html",
    "content": "{% extends \"base_AdminLTE.html\" %}\n{% block title %}Docklet | Create Notification{% endblock %}\n\n{% block panel_title %}Add New Notifications{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n    <li>\n        <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n    </li>\n    <li>\n        <a href=\"/notification/\"><i class=\"fa fa-envelope\"></i>Notifications</a>\n    </li>\n    <li class=\"active\">\n        <strong>Add New Notifications</strong>\n    </li>\n</ol>\n{% endblock %}\n\n{##}\n{% block content %}\n<div class=\"row\">\n    <div class=\"col-lg-12\">\n        <div class=\"box box-info\">\n            <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Add New Notifications</h3>\n\n                <div class=\"box-tools pull-right\">\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\">\n                        <i class=\"fa fa-minus\"></i>\n                    </button>\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\">\n                        <i class=\"fa fa-times\"></i>\n                    </button>\n                </div>\n            </div>\n\n            <div class=\"box-body\">\n                <form id=\"notificationForm\" class=\"form-horizontal\" action=\"/notification/create/\" method=\"post\">\n                    <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-2 control-label\">Title</label>\n                        <div class=\"col-sm-10\">\n                            <input type=\"text\" class=\"form-control\" name=\"title\" id=\"title\">\n                        </div>\n                    </div>\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-2 control-label\">Content</label>\n                        <div class=\"col-sm-10\">\n                            <textarea class=\"form-control\" name=\"content\" id=\"title\"></textarea>\n                        </div>\n                    </div>\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-2 control-label\">Groups</label>\n                        <div class=\"checkbox col-sm-10\">\n                            <label style=\"display: none\"><input type=\"checkbox\" checked name=\"groups\" value=\"none\" style=\"display: none\"></label>\n                            <label><input type=\"checkbox\" name=\"groups\" value=\"all\">all&nbsp;&nbsp;</label>\n                            {% for group_name in groups %}\n                            <label><input type=\"checkbox\" name=\"groups\" value=\"{{ group_name }}\">{{ group_name }}&nbsp;&nbsp;</label>\n                            {% endfor %}\n                        </div>\n                    </div>\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-2 control-label\">Also send email</label>\n                        <div class=\"col-sm-10\">\n                            <input type=\"checkbox\" name=\"sendMail\" value=\"true\">\n                        </div>\n                    </div>\n                    <div class=\"form-group\">\n                        <div class=\"col-sm-4 col-sm-offset-2\">\n\t\t\t\t\t\t    <button class=\"btn btn-primary\" type=\"submit\">Create</button>\n\t\t\t\t\t    </div>\n                    </div>\n                </form>\n            </div>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/dashboard.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n{% block title %}Docklet | Workspace{% endblock %}\r\n\r\n{% block panel_title %}Workspace{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n  <li class=\"active\">\r\n      <strong>Workspace</strong>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n{% block content %}\r\n<div class=\"row\">\r\n         <div class=\"col-lg-12\">\r\n           <div class=\"box box-info\">\r\n                <div class=\"box-header with-border\">\r\n                    <h3 class=\"box-title\">Workspace</h3>\r\n\r\n                  <div class=\"box-tools pull-right\">\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\r\n                    </button>\r\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\r\n                  </div>\r\n                </div>\r\n                 <div class=\"box-body\">\r\n\r\n\t\t\t\t\t <p>\r\n\t\t\t\t\t <a href=\"/workspace/create/\"><button type=\"button\" class=\"btn btn-primary btn-sm\"><i class=\"fa fa-plus\"></i> Add Workspace</button></a>\r\n\t\t\t\t\t </p>\r\n                     <table class=\"table table-bordered\">\r\n                         <thead>\r\n                         <tr>\r\n                             <th>ID</th>\r\n                             <th>Name</th>\r\n\t\t\t     <th>Status</th>\r\n\t\t\t     <th>Operation</th>\r\n\t\t\t     <th>WorkSpace</th>\r\n\t\t\t     <th>Location</th>\r\n                         </tr>\r\n                         </thead>\r\n                         <tbody>\r\n\t\t\t{% for master in allclusters %}\r\n                         {% for cluster in allclusters[master] %}\r\n                         <tr>\r\n\t\t\t\t\t\t\t <td>{{ cluster['id'] }}</td>\r\n                             <td><a href=\"/config/\">{{ cluster['name'] }}</a></td>\r\n\t\t\t     {% if cluster['status'] == 'running' %}\r\n\t\t\t     <td><a href=\"/vclusters/\"><div class=\"text-success\"><i class=\"fa fa-play\"></i> Running</div></a></td>\r\n\t\t\t     <td>\r\n\t\t\t\t     <a href=\"/workspace/{{master.split(\"@\")[0]}}/stop/{{ cluster['name'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-warning\"> &nbsp;Stop&nbsp;&nbsp; </button></a>\r\n\t\t\t\t     <button type=\"button\" class=\"btn btn-xs btn-default\"> Delete </button>\r\n\t\t\t     </td>\r\n\t\t\t\t <td>\r\n\t\t\t\t\t <a href=\"/{{ cluster['proxy_public_ip'] }}/go/{{ mysession['username'] }}/{{ cluster['name'] }}\" target=\"_blank\"><button type=\"button\" class=\"btn btn-xs btn-success\">&nbsp;&nbsp;&nbsp;Go&nbsp;&nbsp;&nbsp;</button></a>\r\n\t\t\t\t</td>\r\n                 {% elif cluster['status'] == 'stopped' %}\r\n\t\t\t     <td><a href=\"/vclusters/\"><div class=\"text-warning\"><i class=\"fa fa-stop\"></i> Stopped</div></a></td>\r\n\t\t\t     <td>\r\n\t\t\t\t     <a href=\"/workspace/{{master.split(\"@\")[0]}}/start/{{ cluster['name'] }}/\"><button type=\"button\" class=\"btn btn-xs btn-success\"> &nbsp;Start&nbsp;</button></a>\r\n\t\t\t\t     <a href=\"/workspace/{{master.split(\"@\")[0]}}/delete/{{ cluster['name'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-danger\">Delete</button></a>\r\n\t\t\t     </td>\r\n\t\t\t\t <td>\r\n\t\t\t\t     <button type=\"button\" class=\"btn btn-xs btn-default\">&nbsp;&nbsp;&nbsp;Go&nbsp;&nbsp;&nbsp;</button>\r\n                 </td>\r\n                 {% else %}\r\n                 <td><a href=\"/vclusters/\"><div class=\"text-warning\"><i class=\"fa fa-times\"></i> Error</div></a></td>\r\n\t\t\t     <td>\r\n\t\t\t\t     <a href=\"/workspace/{{master.split(\"@\")[0]}}/stop/{{ cluster['name'] }}/\"><button type=\"button\"  class=\"btn btn-xs btn-warning\"> &nbsp;Stop&nbsp;&nbsp; </button></a>\r\n\t\t\t\t     <button type=\"button\" class=\"btn btn-xs btn-default\"> Delete </button>\r\n\t\t\t     </td>\r\n\t\t\t\t <td>\r\n                    <button type=\"button\" class=\"btn btn-xs btn-default\">&nbsp;&nbsp;&nbsp;Go&nbsp;&nbsp;&nbsp;</button>\r\n                </td>\r\n\t\t\t {% endif %}\r\n\t\t\t <td><a href=\"/masterdesc/{{master.split(\"@\")[1]}}/\" target=\"_blank\" title=\"{{desc[master.split(\"@\")[1]]}}\">{{master.split(\"@\")[1]}}</a></td>\r\n                         </tr>\r\n                         {% endfor %}\r\n\t\t\t {% endfor %}\r\n                         </tbody>\r\n                     </table>\r\n\r\n                 </div>\r\n             </div>\r\n         </div>\r\n\t</div>\r\n\r\n{% endblock %}\r\n{% block script_src %}\r\n\r\n<script type=\"text/javascript\">\r\nfunction sendAdd(){\r\n    document.getElementById(\"addForm\").submit();\r\n}\r\nfunction sendDel(){\r\n    document.getElementById(\"delForm\").submit();\r\n}\r\n</script>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/description.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n{% block title %}Docklet | Description{% endblock %}\r\n\r\n{% block panel_title %}Description{% endblock %}\r\n\r\n{% block panel_list %}{% endblock %}\r\n\r\n{% block content %}\r\n<pre>{{description}}</pre>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/error/401.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n\r\n\r\n{% block title %}Docklet | Error{% endblock %}\r\n\r\n{% block panel_title %}401 Error Page{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n\r\n{% block content %}\r\n\r\n<div class=\"error-page\">\r\n    <h2 class=\"headline text-red\">401</h2>\r\n\r\n        <div class=\"error-content\">\r\n          <h3><br/><i class=\"fa fa-warning text-red\"></i> Unauthorized Action</h3>\r\n\r\n          <p>\r\n            Sorry, but you did not have the authorizaion for that action, you can go back to\r\n            <a href=\"/dashboard/\">dashboard</a> or <a href=\"/logout\">log out</a>\r\n          </p>\r\n        </div>\r\n</div>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/error/500.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n\r\n\r\n{% block title %}Docklet | Error{% endblock %}\r\n\r\n{% block panel_title %}500 Error Page{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n\r\n{% block content %}\r\n\r\n<div class=\"error-page\">\r\n    <h2 class=\"headline text-red\">500</h2>\r\n\r\n        <div class=\"error-content\">\r\n          <h3><br/><i class=\"fa fa-warning text-red\"></i> {{ title }}</h3>\r\n\r\n          <p>\r\n            {{reason|safe}}\r\n          </p>\r\n        </div>\r\n</div>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/error.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n{% block title %}Docklet | Error{% endblock %}\r\n\r\n{% block panel_title %}Error{% endblock %}\r\n\r\n{% block panel_list %}{% endblock %}\r\n\r\n{% block content %}\r\n<pre>{{message}}</pre>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/home.template",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\t<head>\n\t\t<meta charset=\"utf-8\">\n\t\t<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\n\t\t<title>Docklet | Home</title>\n\t\t<link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\n\n\t\t<link href=\"https://cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\n\t\t<link href=\"https://cdn.bootcss.com/font-awesome/4.5.0/css/font-awesome.css\" rel=\"stylesheet\">\n\n\t\t<style type=\"text/css\">\n\t\ta.linkbtn, a.linkbtn:visited, a.linkbtn:active{\n\t\tcolor:white;\n\t\tpadding:8px;\n\t\t}\n\n\t\ta.linkbtn:hover{\n\t\t\tcolor:white;\n\t\t\ttext-decoration:none;\n\t\t\tborder-bottom: 1px solid white;\n\n\t\t}\n\t\t.navbar-custom-top{\n\t\t\tbackground:transparent;\n\t\t\tposition:absolute;\n\t\t\tz-index:1030;\n\t\t\tleft:0px;\n\t\t\tright:0px;\n\t\t}\n\t\t</style>\n\n\t</head>\n\n\t<body>\n\t\t<div class=\"navbar navbar-custom-top\" role=\"navigation\">\n\t\t\t<div class=\"container\">\n\t\t\t\t<div class=\"row\" style=\"font-size:16px; color:white; padding:16px\">\n\t\t\t\t\t<div class=\"pull-right\" >\n\t\t\t\t\t\t<a class=\"linkbtn\" href=\"https://unias.github.io/docklet/userguide/\">Document</a>\n\t\t\t\t\t\t&centerdot;\n\t\t\t\t\t\t<a class=\"linkbtn\" href=\"/login/\" >Sign In</a>\n\t\t\t\t\t</div>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<a class=\"linkbtn\" href=\"https://docklet.unias.org\"><strong>Docklet Cloud OS</strong></a>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\n\t\t<!-- Carousel -->\n\t\t<div id=\"myCarousel\" class=\"carousel slide\" data-ride=\"carousel\" style=\"box-shadow:0px 2px 10px 0px black\">\n\t\t\t<!-- Indicators -->\n\t\t\t<ol class=\"carousel-indicators\">\n\t\t\t\t<li data-target=\"#myCarousel\" data-slide-to=\"0\" class=\"active\"></li>\n\t\t\t\t<li data-target=\"#myCarousel\" data-slide-to=\"1\"></li>\n\t\t\t</ol>\n\t\t\t<div class=\"carousel-inner\" role=\"listbox\">\n\t\t\t\t<div class=\"item active\">\n\t\t\t\t\t<img src=\"/static/img/home/cloud.png\" alt=\"Cloud OS\" style=\"min-height:600px\">\n\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t<div class=\"carousel-caption\">\n\t\t\t\t\t\t\t<h1>Cloud OS</h1>\n\t\t\t\t\t\t\t<p class=\"lead\">Cloud OS for your cloud Apps.</p>\n\t\t\t\t\t\t\t<br>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"item\">\n\t\t\t\t\t<img src=\"/static/img/home/workspace.png\" alt=\"Cloud Workspace\" style=\"min-height:600px\">\n\t\t\t\t\t<div class=\"container\">\n\t\t\t\t\t\t<div class=\"carousel-caption\">\n\t\t\t\t\t\t\t<h1>Cloud Workspace</h1>\n\t\t\t\t\t\t\t<p class=\"lead\">Workspace in cloud, all your work in cloud.</p>\n\t\t\t\t\t\t\t<br>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<a class=\"left carousel-control\" href=\"#myCarousel\" role=\"button\" data-slide=\"prev\">\n\t\t\t\t<span class=\"glyphicon glyphicon-chevron-left\" aria-hidden=\"true\"></span>\n\t\t\t\t<span class=\"sr-only\">Previous</span>\n\t\t\t</a>\n\t\t\t<a class=\"right carousel-control\" href=\"#myCarousel\" role=\"button\" data-slide=\"next\">\n\t\t\t\t<span class=\"glyphicon glyphicon-chevron-right\" aria-hidden=\"true\"></span>\n\t\t\t\t<span class=\"sr-only\">Next</span>\n\t\t\t</a>\n\t\t</div><!-- /.carousel -->\n\n\t\t<div class=\"container\" style=\"margin-top:60px\">\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-lg-6 col-lg-offset-1 col-md-7 col-md-offset-0 col-sm-7 col-sm-offset-0 col-xs-12 col-xs-offset-0\">\n\t\t\t\t\t<h2>Workspace=Cluster+Service+Data</h2>\n\t\t\t\t\t<br/>\n\t\t\t\t\t<p class=\"lead\">Package service and data based on virtual cluster as virtual compute environment for your work.\n\t\t\t\t\t<br> This is your Workspace !</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-lg-3 col-lg-offset-1 col-md-4 col-md-offset-1 col-sm-5 col-sm-offset-0 col-xs-10 col-xs-offset-1\">\n\t\t\t\t\t<img src=\"/static/img/home/app-workspace.png\" alt=\"feature-workspace\" width=\"100%\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-lg-3 col-lg-offset-1 col-md-4 col-md-offset-1 col-sm-5 col-sm-offset-0 col-xs-10 col-xs-offset-1\">\n\t\t\t\t\t<img src=\"/static/img/home/app-dist.png\" alt=\"feature-app\" width=\"100%\">\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-lg-6 col-lg-offset-1 col-md-7 col-md-offset-0 col-sm-7 col-sm-offset-0 col-xs-12 col-xs-offset-0\">\n\t\t\t\t\t<h2>Click and Go</h2>\n\t\t\t\t\t<br/>\n\t\t\t\t\t<p class=\"lead\">Distributed or single node ? Never mind !\n\t\t\t\t\tClick it just like start an app on your smart phone, and your workspace is ready for you.</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\t\t\t<div class=\"row\">\n\t\t\t\t<div class=\"col-lg-6 col-lg-offset-1 col-md-7 col-md-offset-0 col-sm-7 col-sm-offset-0 col-xs-12 col-xs-offset-0\">\n\t\t\t\t\t<h2>All in Web</h2>\n\t\t\t\t\t<br/>\n\t\t\t\t\t<p class=\"lead\">All you need is a web browser. Compute in web, code in web, plot in web, anything in web !\n\t\t\t\t\tYou can get to work anytime and anywhere by internet.</p>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-lg-3 col-lg-offset-1 col-md-4 col-md-offset-1 col-sm-5 col-sm-offset-0 col-xs-10 col-xs-offset-1\">\n\t\t\t\t\t<img src=\"/static/img/home/app-web.png\" alt=\"feature-web\" width=\"100%\">\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t\t<hr>\n\n\t\t\t<footer>\n\t\t\t\t<p class=\"pull-right\">Powered by <a href=\"https://github.com/unias/docklet/\" style=\"color:blue\">Docklet</a></p>\n\t\t\t\t<p>&copy; SEI, PKU</p>\n\t\t\t</footer>\n\n\t\t</div>\n\n\t\t<script src=\"https://cdn.bootcss.com/jquery/2.2.1/jquery.js\"></script>\n\t\t<script src=\"https://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\n\t</body>\n</html>\n"
  },
  {
    "path": "web/templates/listcontainer.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Container{% endblock %}\n\n{% block panel_title %}ContainerInfo{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/index/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n      <li class=\"active\">\n      <strong>ContainerInfo</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block content %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Cluster Name: {{ clustername }}</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body\">\n\t\t\t<p>\n\t\t\t<a href=\"/cluster/scaleout/{{ clustername }}\"><button type=\"button\" class=\"btn btn-primary btn-sm\"><i class=\"fa fa-plus\"></i>Add Container</button></a>\n\t\t\t</p>\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>Node ID</th>\n\t\t\t\t <th>Node Name</th>\n                                 <th>IP Address</th>\n                                 <th>Status</th>\n\t\t\t\t <th>Last Save</th>\n\t\t\t\t <th>Image</th>\n\t\t\t\t <th>Detail</th>\n\t\t\t\t <th>Flush</th>\n\t\t\t\t <th>Save</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for container in containers %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ container['containername'] }}</td>\n                                 <td>{{ container['ip'] }}</td>\n\n                                 {% if  status  == 'stopped' %}\n                                 <td><div class=\"label label-danger\">Stopped</div></td>\n                                 {% else %}\n                                 <td><div class=\"label label-primary\">Running</div></td>\n                                 {% endif %}\n\n\t\t\t\t <td>{{ container['lastsave'] }}</td>\n\t\t\t\t <td>{{ container['image'] }}</td>\n                                 <td><a class=\"btn btn-info\" href='/monitor/Node/{{ container['containername'] }}/detail/'>Detail</a></td>\n\t\t\t\t <td><a class=\"btn btn-warning\" href=\"/cluster/flush/{{ clustername }}/{{ container['containername'] }}/\">Flush</a></td>\n\t\t\t\t <td><button type=\"button\" class=\"btn btn-success btn-sm btn-block\" data-toggle=\"modal\" data-target=\"#DelModal_{{ container['containername'] }}\"> save</button></td>\n\t\t\t\t<div class=\"modal inmodal\" id=\"DelModal_{{ container['containername'] }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                   <div class=\"modal-dialog\">\n                                   <div class=\"modal-content animated fadeIn\">\n                                           <div class=\"modal-header\">\n                                               <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                               <i class=\"fa fa-trash modal-icon\"></i>\n                                               <h4 class=\"modal-title\">Save Image</h4>\n                                               <small class=\"font-bold\">Save Your Environment As a Image</small>\n                                           </div>\n                                           <div class=\"modal-body\">\n                                                <div class=\"form-group\">\n                                                  <form action=\"/cluster/save/{{ clustername }}/{{ container['containername'] }}/\" method=\"GET\" id=\"saveImage\">\n                                                  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                                  <label>Image Name</label>\n\t\t\t\t\t\t  <input type=\"text\" placeholder=\"Enter image name\" class=\"form-control\" name=\"ImageName\" id=\"ImageName\"/>\n                                           \t  <div class=\"modal-footer\">\n                                               \t\t<button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                               \t\t<button type=\"submit\" class=\"btn btn-success\">Save</button>\n                                                  </div>\n                                                  </form>\n                                                </div>\n                                           </div>\n                                       </div>\n                                   </div>\n                               </div>\n                               </div>\n\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/login.html",
    "content": "\r\n<!DOCTYPE html>\r\n<html>\r\n<head>\r\n  <meta charset=\"utf-8\">\r\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n  <title>Docklet | Login</title>\r\n  <!-- Tell the browser to be responsive to screen width -->\r\n  <meta content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" name=\"viewport\">\r\n\r\n  <!-- Bootstrap 3.3.5 -->\r\n  <link href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\r\n  <!-- Font Awesome -->\r\n  <link href=\"//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css\" rel=\"stylesheet\">\r\n  <!-- Ionicons -->\r\n  <link href=\"//cdn.bootcss.com/ionicons/2.0.1/css/ionicons.min.css\" rel=\"stylesheet\">\r\n  <!-- Theme style -->\r\n  <link rel=\"stylesheet\" href=\"/static/dist/css/AdminLTE.min.css\">\r\n\r\n  <link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\r\n\r\n\r\n</head>\r\n<body class=\"hold-transition login-page\">\r\n<div class=\"login-box\">\r\n  <div class=\"login-logo\">\r\n    <img src=\"/static/img/logo.png\" class=\"logo-name\" height=\"50%\" width=\"50%\">\r\n    <!--a href=\"/\"><b>Docklet</b></a-->\r\n  </div>\r\n  <!-- /.login-logo -->\r\n  <div class=\"login-box-body\">\r\n    <p class=\"login-box-msg\">An easy and quick way to launch your DISTRIBUTED applications!</p>\r\n\r\n    <form action=\"\" method=\"POST\" id=\"loginForm\">\r\n      <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\r\n      <div class=\"form-group has-feedback\">\r\n        <span class=\"glyphicon glyphicon-user form-control-feedback\"></span>\r\n        <input type=\"text\" class=\"form-control\" placeholder=\"Username\" name=\"username\" required>\r\n      </div>\r\n      <div class=\"form-group has-feedback\">\r\n        <span class=\"glyphicon glyphicon-lock form-control-feedback\"></span>\r\n        <input type=\"password\" class=\"form-control\" placeholder=\"Password\" name=\"password\" required>\r\n      </div>\r\n      <div class=\"row\">\r\n        <div class=\"col-xs-12\">\r\n          <p style=\"text-align:center;font-weight:800;color:red;\">{{ loginMsg }}</p>\r\n        </div>\r\n      </div>\r\n      <div class=\"row\">\r\n        <div class=\"col-xs-12\">\r\n          <button type=\"submit\" class=\"btn btn-primary btn-block btn-flat\">Sign In</button>\r\n        </div>\r\n      </div>\r\n    </form>\r\n    {% if open_registry == \"True\" %}\r\n    <br/>\r\n      <div class=\"row\">\r\n        <div class=\"col-xs-12\">\r\n\t\t<a href=\"/register/\"><button class=\"btn btn-primary btn-block btn-flat\">Register</button></a>\r\n        </div>\r\n      </div>\r\n    {% endif %}\r\n\r\n    <div class=\"social-auth-links text-center\">\r\n      <!--p>- OR -</p>\r\n      <a href=\"#\" class=\"btn btn-block btn-social btn-facebook btn-flat\"><i class=\"fa fa-facebook\"></i> Sign in using\r\n        Facebook</a>\r\n      <a href=\"#\" class=\"btn btn-block btn-social btn-google btn-flat\"><i class=\"fa fa-google-plus\"></i> Sign in using\r\n        Google+</a-->\r\n      <a href=\"{{ url }}\">{{ link }}</a>\r\n    </div>\r\n\r\n  </div>\r\n  <!-- /.login-box-body -->\r\n</div>\r\n<!-- /.login-box -->\r\n\r\n<!-- jQuery 2.2.1 -->\r\n<script src=\"//cdn.bootcss.com/jquery/2.2.1/jquery.min.js\"></script>\r\n<!-- Bootstrap 3.3.5 -->\r\n<script src=\"//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\r\n<script src=\"//cdn.bootcss.com/jquery-validate/1.17.0/jquery.validate.js\"></script>\r\n<script>\r\n$().ready(function() {\r\n    $(\"#loginForm\").validate();\r\n});\r\n</script>\r\n\r\n</body>\r\n</html>\r\n"
  },
  {
    "path": "web/templates/logs.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\n{% block title %}Docklet | Logs{% endblock %}\n\n{% block panel_title %}Logs{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Logs</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n\n<link href=\"/static/dist/css/filebox.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n{% block content %}\n\n\n<div class=\"row\">\n<div class=\"col-md-12\">\n  <div class=\"box box-primary\">\n      <div class=\"box-header with-border\">\n         <h3 class=\"box-title\">Logs</h3>\n      </div>\n      <div class=\"box-body\">\n          {% for filename in logs %}\n            <div class=\"file-box\">\n                <div class=\"file\">\n                    <a href=\"/logs/{{ filename }}/\">\n                        <span class=\"corner\"></span>\n\n                        <div class=\"icon\">\n                            <i class=\"fa fa-file\"></i>\n                        </div>\n                        <div class=\"file-name\">\n                            {{ filename }}\n                        </div>\n                    </a>\n                </div>\n            </div>\n          {% endfor %}\n      </div>\n  </div>\n</div>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/history.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | History{% endblock %}\n\n{% block panel_title %}History of All Created VNodes{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>History</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"/static/dist/css/flotconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div class=\"row\">\n<div class=\"col-md-12\">\n<div class=\"box box-info\">\n<div class=\"box-header with-border\">\n       <h3 class=\"box-title\">All Created VNodes</h3>\n       <div class=\"box-tools pull-right\">\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n         </button>\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n       </div>\n     </div>\n                 <div class=\"box-body\">\n                   <div class=\"table\">\n                     <table width=\"100%\" cellspacing=\"0\" style=\"margin:0 auto;\" class=\"table table-striped table-bordered table-hover table-history\" >\n                        <thead>\n                    \t<tr>\n                        <th>NO</th>\n                        <th>VNode name</th>\n                        <th>Total billing</th>\n\t\t\t<th>Location</th>\n                        <th>History</th>\n                    \t</tr>\n                    \t</thead>\n                    <tbody>\n\t\t      {% for master in allvnodes %}\n                      {% for vnode in allvnodes[master] %}\n                      <tr>\n                      <th>{{ loop.index }}</th>\n                      <th>{{ vnode.name }}</th>\n                      <th>{{ vnode.billing }} beans</th>\n\t\t      <th>{{ master.split(\"@\")[1] }}</th>\n\t\t      <td><a class=\"btn btn-info btn-xs\" href='/history/{{master.split(\"@\")[0]}}/{{ vnode.name }}/'>History</a></td>\n                      </tr>\n                      {% endfor %}\n\t\t      {% endfor %}\n                    </tbody>\n\t\t  </table>\n                 </div>\n\t     </div>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n\n<script>\n         $(document).ready(function() {\n            $(\".table-history\").DataTable({\"scrollX\":true});\n         });\n\n </script>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/historyVNode.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | History{% endblock %}\n\n{% block panel_title %}History of <div id='node_name'>{{ vnode_name }}</div>{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/history/'>History</a>\n  </li>\n  <li class=\"active\">\n      <strong>History of Vnode</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"/static/dist/css/flotconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div class=\"row\">\n<div class=\"col-md-12\">\n<div class=\"box box-info\">\n     <div class=\"box-header with-border\">\n       <h3 class=\"box-title\">History</h3>\n       <div class=\"box-tools pull-right\">\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n         </button>\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n       </div>\n     </div>\n                 <div class=\"box-body\">\n                   <div class=\"table table-responsive\">\n                     <table class=\"table table-striped table-bordered table-hover table-image\" >\n                        <thead>\n                    \t<tr>\n                        <th>History ID</th>\n                        <th>Action</th>\n                        <th>Running Time</th>\n                        <th>Cpu Time</th>\n                        <th>Billing</th>\n                        <th>Action Time</th>\n                    \t</tr>\n                    \t</thead>\n                    <tbody>\n                      {% for record in history %}\n                      <tr>\n                      <th>{{ record['id'] }}</th>\n                      <th>{{ record['action'] }}</th>\n                      <th>{{ record['runningtime'] }} seconds</th>\n                      <th>{{ record['cputime'] }} seconds</th>\n                      <th>{{ record['billing'] }} beans</th>\n                      <th>{{ record['actionTime'] }}</th>\n                      </tr>\n                      {% endfor %}\n                    </tbody>\n\t\t  </table>\n                 </div>\n\t      </div>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n\n<script>\n         $(document).ready(function() {\n            $(\".table-image\").DataTable();\n         });\n\n </script>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/hosts.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Hosts{% endblock %}\n\n{% block panel_title %}Hosts Info{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n      <li class=\"active\">\n      <strong>HostsInfo</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<ul class=\"nav nav-tabs\" role=\"tablist\" id=\"myTabs\">\n{% for master in allmachines %}\n{% if loop.index == 1%}\n<li role=\"presentation\" class=\"active\"><a href=\"#{{master.split(\"@\")[1]}}\" data-toggle=\"tab\" aria-controls=\"{{master.split(\"@\")[1]}}\">{{master.split(\"@\")[1]}}</a></li>\n{% else %}\n<li role=\"presentation\"><a href=\"#{{master.split(\"@\")[1]}}\" data-toggle=\"tab\" aria-controls=\"{{master.split(\"@\")[1]}}\">{{master.split(\"@\")[1]}}</a></li>\n{% endif %}\n{% endfor %}\n</ul>\n<div id=\"myTabContent\" class=\"tab-content\">\n{% for master in allmachines %}\n{% if loop.index == 1 %}\n<div role=\"tabpanel\" class=\"tab-pane active\" aria-labelledby=\"{{master.split(\"@\")[1]}}\" id=\"{{master.split(\"@\")[1]}}\">\n{% else %}\n<div role=\"tabpanel\" class=\"tab-pane\" aria-labelledby=\"{{master.split(\"@\")[1]}}\" id=\"{{master.split(\"@\")[1]}}\">\n{% endif %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n\t\t      <h3 class=\"box-title\">{{master.split(\"@\")[1]}}</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n\t\t\t     <p>\n\t\t\t     <a href=\"/cloud/{{master.split(\"@\")[0]}}/node/add/\"><button type=\"button\" class=\"btn btn-primary btn-sm\"><i class=\"fa fa-plus\"></i>Add Host</button></a>\n\t\t\t     </p>\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>NO</th>\n                                 <th>IP Address</th>\n                                 <th>Status</th>\n\t\t\t\t <th>Nodes running</th>\n\t\t\t\t <th>Cpu used</th>\n\t\t\t\t <th>Mem used</th>\n\t\t\t\t <th>Disk used</th>\n\t\t\t\t <th>Summary</th>\n\t\t\t\t <th>Operation</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for phym in allmachines[master] %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ phym['ip'] }}</td>\n                                 {% if phym['status'] == 'STOPPED' %}\n\t\t\t\t <td><div id='{{master.split(\"@\")[1]}}_{{ loop.index }}_status' class=\"label label-danger\">Stopped</div></td>\n                                 {% else %}\n\t\t\t\t <td><div id='{{master.split(\"@\")[1]}}_{{ loop.index }}_status' class=\"label label-primary\">Running</div></td>\n                                 {% endif %}\n                                 <td>\n\t\t\t\t\t <label id='{{master.split(\"@\")[1]}}_{{ loop.index }}_conrunning'>{{ phym['containers']['running'] }}</label> /\n\t\t\t\t\t <a href='/hosts/{{master.split(\"@\")[0]}}/{{ phym['ip'] }}/containers/' id='{{master.split(\"@\")[1]}}_{{ loop.index }}_contotal' >{{ phym['containers']['total'] }}</a>\n\t\t\t\t </td>\n\t\t\t\t <td id='{{master.split(\"@\")[1]}}_{{ loop.index }}_cpu'>--</td>\n\t\t\t\t <td id='{{master.split(\"@\")[1]}}_{{ loop.index }}_mem'>--</td>\n\t\t\t\t <td id='{{master.split(\"@\")[1]}}_{{ loop.index }}_disk'>--</td>\n\t\t\t\t <td><a class=\"btn btn-info btn-xs\" href='/hosts/{{master.split(\"@\")[0]}}/{{ phym['ip'] }}/'>Realtime</a></td>\n\t\t\t\t <td><button class=\"btn btn-xs btn-default\">Delete</button>\n\t\t\t\t\t <button class=\"btn btn-xs btn-danger\" data-toggle=\"modal\" data-target=\"#Migrate_{{loop.index}}_{{master.split(\"@\")[1]}}\">Migrate</button>\n\t\t\t\t\t <div class=\"modal inmodal\" id=\"Migrate_{{loop.index}}_{{master.split(\"@\")[1]}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n\t                                  <div class=\"modal-dialog\">\n        \t                          <div class=\"modal-content animated fadeIn\">\n                                          <div class=\"modal-header\">\n                                               <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                               <i class=\"fa fa-plus modal-icon\"></i>\n\t\t\t\t\t       <h4 class=\"modal-title\">Migrate Container from {{phym['ip']}} to Another Hosts [dangerous!!!]</h4>\n                                           </div>\n                                           <div class=\"modal-body\">\n\t\t\t\t\t\t   <form action=\"/hosts/{{master.split(\"@\")[0]}}/migrate/{{ phym['ip'] }}/\" method=\"POST\" id=\"MigrateForm\">\n                    \t\t\t\t\t<input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                \t\t\t<table class=\"table table-striped table-bordered table-hover table-image\">\n\t\t\t\t\t\t\t\t<thead>\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<th>HostIp</th>\n\t\t\t\t\t\t\t\t\t\t<th>Nodes</th>\n\t\t\t\t\t\t\t\t\t\t<th>choose</th>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t</thead>\n\t\t\t\t\t\t\t\t<tbody>\n                             \t\t\t\t\t\t{% for tphym in allmachines[master] %}\n\t\t\t\t\t\t\t\t\t{% if not phym == tphym %}\n\t\t\t\t\t\t\t\t\t<tr>\n\t\t\t\t\t\t\t\t\t\t<td>{{ tphym['ip'] }}</td>\n\t\t\t\t\t\t\t\t\t\t<td>{{ tphym['containers']['running']}} / {{ tphym['containers']['total'] }}</td>\n\t\t\t\t\t\t\t\t\t\t<td><input type=\"checkbox\" name=\"target\" value=\"{{tphym['ip']}}\"></td>\n\t\t\t\t\t\t\t\t\t</tr>\n\t\t\t\t\t\t\t\t\t{% endif %}\n\t\t\t\t\t\t\t\t\t{% endfor %}\n\t\t\t\t\t\t\t\t</tbody>\n\t\t\t\t\t\t\t</table>\n\t\t\t\t\t\t\t<div class=\"modal-footer\">\n                                                        \t<button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                                        \t<button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                                                   \t</div>\n                                                  </form>\n                                              </div>\n                                              </div>\n                                         </div>\n\t\t\t\t\t </div>\n\t\t\t\t </div>\n\t\t\t\t </td>\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n</div>\n{% endfor %}\n{% endblock %}\n\n{% block script_src %}\n<script type='text/javascript'>\n   function update(url,index)\n   {\n\n       var MB = 1024;\n       $.post(url+\"/status/\",{},function(data){\n\t       console.log(data);\n\t\tvar status = data.monitor.status;\n\t\tif(status == 'RUNNING')\n       \t\t{\n           \t    var tmp = $(\"#\"+index+\"_status\");\n                    tmp.removeClass();\n\t            tmp.addClass(\"label label-primary\");\n\t   \t    tmp.html(\"Running\");\n       \t\t}\n       \t\telse if(status == 'STOPPED')\n       \t\t{\n           \t    var tmp = $(\"#\"+index+\"_status\");\n           \t    tmp.removeClass();\n\t   \t    tmp.addClass(\"label label-danger\");\n\t   \t    tmp.html(\"Stopped\");\n       \t\t}\n\n       \t\t$.post(url+\"/containers/\",{},function(data){\n       \t\t\tvar containers = data.monitor.containers;\n       \t\t\t$(\"#\"+index+\"_contotal\").html(containers.total);\n       \t\t\t$(\"#\"+index+\"_conrunning\").html(containers.running);\n       \t\t\t},\"json\");\n\n\t\tif(status == 'STOPPED')\n\t\t{\n\t\t       \t$(\"#\"+index+\"_cpu\").html('--');\n\t\t       \t$(\"#\"+index+\"_mem\").html('--');\n\t\t       \t$(\"#\"+index+\"_disk\").html('--');\n\t\t\treturn;\n\t\t}\n\n       \t\t$.post(url+\"/cpuinfo/\",{},function(data){\n\t\t       \tvar idle = data.monitor.cpuinfo.idle;\n\t\t\t    var usedp = (100 - idle).toFixed(2);\n\t\t       \t$(\"#\"+index+\"_cpu\").html(String(usedp)+\"%\");\n\t\t       \t},\"json\");\n\n       \t\t$.post(url+\"/meminfo/\",{},function(data){\n\t\t\tvar used = data.monitor.meminfo.used;\n\t\t\tvar total = data.monitor.meminfo.total;\n\t\t\tvar usedp = String(((used/total)*100).toFixed(2))+\"%\";\n\t\t       \t$(\"#\"+index+\"_mem\").html(usedp);\n\t\t       \t},\"json\");\n\n  \t     \t$.post(url+\"/diskinfo/\",{},function(data){\n\t\t       \tvar val = data.monitor.diskinfo;\n                var usedp = val[0].percent;\n\t\t       \t$(\"#\"+index+\"_disk\").html(String(usedp)+\"%\");\n\t               \t},\"json\");\n       \t\t},\"json\");\n   }\n\n   function updateAll()\n   {\n        var host = window.location.host;\n   {% for master in allmachines %}\n   {% for phym in allmachines[master] %}\n   \turl = \"//\" + host + \"/monitor/\" + '{{master.split(\"@\")[0]}}' + \"/hosts/\" + '{{phym[\"ip\"]}}';\n        // url = url0 + '{{ phym['ip'] }}';\n\tupdate(url,'{{master.split(\"@\")[1]}}_{{ loop.index }}');\n   {% endfor %}\n   {% endfor %}\n   }\n   setInterval(updateAll,5000);\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/hostsConAll.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Hosts{% endblock %}\n\n{% block panel_title %}Node list for {{ com_ip }}{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/hosts/'>Hosts</a>\n  </li>\n  <li class=\"active\">\n      <strong>Node List</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block content %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Total Nodes</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t                 <th>NO</th>\n\t\t\t\t                 <th>Name</th>\n                                 <th>Owner</th>\n                                 <th>Owner's Truename</th>\n                                 <th>State</th>\n                                 <th>PID</th>\n                                 <th>IP Address</th>\n\t\t\t\t                 <th>Cpu used</th>\n                                 <th>%Cpu</th>\n                                 <th>Mem used</th>\n                                 <th>Summary</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for container in containerslist %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ container['Name'] }}</td>\n                                 <td>{{ container['owner']['username'] }}</td>\n                                 <td>{{ container['owner']['truename'] }}</td>\n                                 {% if container['State'] == 'STOPPED' %}\n                                 <td><div id='{{ container['Name'] }}_state' class=\"label label-danger\">Stopped</div></td>\n                                 <td id='{{ container['Name'] }}_pid'>--</td>\n                                 <td id='{{ container['Name'] }}_ip'>--</td>\n                                 {% else %}\n                                 <td><div id='{{ container['Name'] }}_state' class=\"label label-primary\">Running</div></td>\n                                 <td id='{{ container['Name'] }}_pid'>{{ container['PID'] }}</td>\n                                 <td id='{{ container['Name'] }}_ip'>{{ container['IP'] }}</td>\n                                 {% endif %}\n                                 <td id='{{ container['Name'] }}_cpu'>--</td>\n                                 <td id='{{ container['Name'] }}_cpupercent'>--</td>\n                                 <td id='{{ container['Name'] }}_mem'>--</td>\n                                 <td><a class=\"btn btn-info btn-xs\"\n                                         href='/hosts/{{ masterip }}/{{ com_ip }}/containers/{{\n                                         container['Name'] }}/'>Realtime</a></td>\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n{% endblock %}\n\n{% block script_src %}\n<script type='text/javascript'>\n   function update(url,name)\n   {\n\n       $.post(url+\"/basic_info/\",{},function(data){\n\t\tvar state = data.monitor.basic_info.State;\n\t\tif(state == 'RUNNING')\n       \t\t{\n           \t    var tmp = $(\"#\"+name+\"_state\");\n                    tmp.removeClass();\n\t            tmp.addClass(\"label label-primary\");\n\t   \t    tmp.html(\"Running\");\n\t\t    $(\"#\"+name+\"_pid\").html(data.monitor.basic_info.PID);\n\t\t    $(\"#\"+name+\"_ip\").html(data.monitor.basic_info.IP);\n       \t\t}\n       \t\telse if(state == 'STOPPED')\n       \t\t{\n           \t    var tmp = $(\"#\"+name+\"_state\");\n           \t    tmp.removeClass();\n\t   \t    tmp.addClass(\"label label-danger\");\n\t   \t    tmp.html(\"Stopped\");\n\t\t    $(\"#\"+name+\"_pid\").html('--');\n\t\t    $(\"#\"+name+\"_ip\").html('--');\n\t\t    $(\"#\"+name+\"_cpu\").html('--');\n\t\t    $(\"#\"+name+\"_mem\").html('--');\n\t\t    return;\n       \t\t}\n\n       \t\t$.post(url+\"/cpu_use/\",{},function(data){\n\t\t       \tvar val = (data.monitor.cpu_use.val).toFixed(2);\n\t\t\t    var unit = data.monitor.cpu_use.unit;\n                var hostpercent = data.monitor.cpu_use.hostpercent;\n                var percent = (hostpercent*100).toFixed(2);\n\t\t       \t$(\"#\"+name+\"_cpu\").html(val +\" \"+ unit);\n\t\t       \t$(\"#\"+name+\"_cpupercent\").html(percent);\n\t\t       \t},\"json\");\n\n\n       \t\t$.post(url+\"/mem_use/\",{},function(data){\n\t\t       \tvar val = data.monitor.mem_use.val;\n\t\t\t    var unit = data.monitor.mem_use.unit\n\t\t       \t$(\"#\"+name+\"_mem\").html(val+\" \"+unit);\n\t\t       \t},\"json\");\n\n       \t\t},\"json\");\n   }\n\n   function updateAll()\n   {\n        var host = window.location.host;\n\t{% for container in containerslist %}\n\turl =  \"//\" + host + \"/monitor/\" + '{{masterip}}' + \"/vnodes/\" + '{{container[\"Name\"]}}';\n        //url = url0 + '{{ container['Name'] }}';\n\tupdate(url,'{{ container['Name'] }}');\n\t{% endfor %}\n    /*$.post(url+\"/concpuinfo/\",{},function(data){\n\t\tvar info = data.monitor.concpuinfo;\n\t\tfor(container in info)\n        {\n\t\t    $(\"#\"+container+\"_cpupercent\").html(info[container]);\n        }\n\t\t},\"json\");*/\n   }\n   setInterval(updateAll,4000);\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/hostsRealtime.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Hosts{% endblock %}\n\n{% block panel_title %}Summary for <div id='com_ip'>{{ com_ip }}</div>{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/hosts/'>Hosts</a>\n  </li>\n  <li class='active'>\n      <strong>Summary</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"/static/dist/css/flotconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div id=\"masterip\" style=\"display:none\">{{masterip}}</div>\n       <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">CPU info</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>Processor ID</th>\n\t\t\t\t <th>Model name</th>\n\t\t\t\t <th>physical id</th>\n\t\t\t\t <th>core id</th>\n\t\t\t\t <th>cpu MHz</th>\n\t\t\t\t <th>cache size</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n\t\t\t     {% for processor in processors %}\n                             <tr>\n\t\t\t\t <th>{{ processor['processor'] }}</th>\n                                 <td>{{ processor['model name']}}</td>\n                                 <td>{{ processor['physical id']}}</td>\n                                 <td>{{ processor['core id']}}</td>\n                                 <td>{{ processor['cpu MHz']}}</td>\n                                 <td>{{ processor['cache size']}}</td>\n\t\t\t     </tr>\n\t\t\t     {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n\t   <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">OS info</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n                         <table class=\"table table-bordered\">\n                             <tbody>\n                             <tr>\n                             <th>OS name</th>\n                                 <td>{{ OSinfo['platform']}}</td>\n                             </tr>\n\t\t\t                 <tr>\n\t\t\t\t             <th>OS node name</th>\n                                 <td>{{ OSinfo['node']}}</td>\n\t\t\t                 </tr>\n\t\t\t                 <tr>\n\t\t\t\t             <th>OS kernel release</th>\n                                 <td>{{ OSinfo['release']}}</td>\n\t\t\t                 </tr>\n\t\t\t                 <tr>\n\t\t\t\t             <th>OS kernel version</th>\n                                 <td>{{ OSinfo['version']}}</td>\n\t\t\t                 </tr>\n\t\t\t                 <tr>\n\t\t\t\t             <th>OS kernel machine</th>\n                                 <td>{{ OSinfo['machine']}}</td>\n\t\t\t                 </tr>\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n          <div class=\"row\">\n\t     <div class=\"col-md-12\">\n                 <div class=\"box box-info\">\n                      <div class=\"box-header with-border\">\n                        <h3 class=\"box-title\">Cpu and Memory Status</h3>\n\n                        <div class=\"box-tools pull-right\">\n                          <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                          </button>\n                          <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                        </div>\n                      </div>\n                     <div class=\"box-body table-responsive\">\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t  <th colspan='4'>Cpu(%)</th>\n\t\t\t\t  <th colspan='3'>Memory(MiB)</th>\n                             </tr>\n                             <tr>\n\t\t\t\t <th>user</th>\n\t\t\t\t <th>system</th>\n\t\t\t\t <th>iowait</th>\n\t\t\t\t <th>idle</th>\n\t\t\t\t <th>used</th>\n\t\t\t\t <th>free</th>\n\t\t\t\t <th>total</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             <tr>\n                                 <td id='cpu_user'>--</td>\n                                 <td id='cpu_system'>--</td>\n                                 <td id='cpu_iowait'>--</td>\n                                 <td id='cpu_idle'>--</td>\n                                 <td id='mem_used'>--</td>\n                                 <td id='mem_free'>--</td>\n                                 <td id='mem_total'>--</td>\n\t\t\t     </tr>\n                             </tbody>\n                         </table>\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n           <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Disk Status</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n                                  <th colspan='5'>Disk info</th>\n                             </tr>\n                             <tr>\n                                  <th>device</th>\n                                  <th>used(MiB)</th>\n                                  <th>free(MiB)</th>\n                                  <th>total(MiB)</th>\n                                  <th>used percent(%)</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for diskinfo in diskinfos %}\n                             <tr>\n                                 <td id='disk_{{ loop.index }}_device'>--</td>\n                                 <td id='disk_{{ loop.index }}_used'>--</td>\n                                 <td id='disk_{{ loop.index }}_free'>--</td>\n                                 <td id='disk_{{ loop.index }}_total'>--</td>\n                                 <td id='disk_{{ loop.index }}_usedp'>--</td>\n                             </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n\t   <div class=\"row\">\n                <div class=\"col-lg-6\">\n                  <div class=\"box box-info\">\n                       <div class=\"box-header with-border\">\n                         <h3 class=\"box-title\">Memory Used(%):</h3>\n\n                         <div class=\"box-tools pull-right\">\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                           </button>\n                           <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                         </div>\n                       </div>\n                        <div class=\"box-body\">\n\n                            <div class=\"flot-chart\">\n                                <div class=\"flot-chart-content\" id=\"mem-chart\"></div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\t\t<div class=\"col-lg-6\">\n      <div class=\"box box-info\">\n           <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">CPU Used(%):</h3>\n\n             <div class=\"box-tools pull-right\">\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n               </button>\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n             </div>\n           </div>\n                        <div class=\"box-body\">\n\n                            <div class=\"flot-chart\">\n                                <div class=\"flot-chart-content\" id=\"cpu-chart\"></div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\t    <!--<div class=\"row\">\n\t\t<div class=\"col-lg-6\">\n                    <div class=\"ibox float-e-margins\">\n                        <div class=\"ibox-title\">\n                            <h5>Disk Used(%):</h5>\n                            <div class=\"ibox-tools\">\n                                <a class=\"collapse-link\">\n                                    <i class=\"fa fa-chevron-up\"></i>\n                                </a>\n                                <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n                                    <i class=\"fa fa-wrench\"></i>\n                                </a>\n                                <ul class=\"dropdown-menu dropdown-user\">\n                                    <li><a href=\"#\">Config option 1</a>\n                                    </li>\n                                    <li><a href=\"#\">Config option 2</a>\n                                    </li>\n                                </ul>\n                                <a class=\"close-link\">\n                                    <i class=\"fa fa-times\"></i>\n                                </a>\n                            </div>\n                        </div>\n                        <div class=\"ibox-content\">\n\n                            <div class=\"flot-chart\">\n                                <div class=\"flot-chart-content\" id=\"disk-chart\"></div>\n                            </div>\n                        </div>\n                    </div>\n                </div>\n\t    </div>-->\n\n\n\n{% endblock %}\n\n{% block script_src %}\n<!-- Flot -->\n<script src=\"//cdn.bootcss.com/flot.tooltip/0.8.6/jquery.flot.tooltip.min.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.resize.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.pie.min.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.time.min.js\"></script>\n\n<!-- Flot demo data -->\n<script src=\"/static/js/plot_monitorReal.js\"></script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/monitorUserAll.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | MonitorUser{% endblock %}\n\n{% block panel_title %}Users Info{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/index/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n      <li class=\"active\">\n      <strong>UsersInfo</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block content %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">All Users Info</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body\">\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>NO</th>\n                                 <th>Name</th>\n\t\t\t\t <th>Running/Total Clusters</th>\n\t\t\t\t <th>Running/Total Containers</th>\n\t\t\t\t <th>Register Time</th>\n\t\t\t\t <th>Last Login</th>\n\t\t\t\t <th>Frequency</th>\n\t\t\t\t <th>Detail</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for user in userslist %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ user['name'] }}</td>\n\t\t\t\t <td>{{ user['clustercnt']['clurun'] }}/{{ user['clustercnt']['clutotal'] }}</td>\n\t\t\t\t <td>{{ user['clustercnt']['conrun'] }}/{{ user['clustercnt']['contotal'] }}</td>\n\t\t\t\t <td>--</td>\n\t\t\t\t <td>--</td>\n\t\t\t\t <td>--</td>\n                                 <td><a class=\"btn btn-info\" href='/monitor/User/{{ user['name'] }}/clusters/'>Clusters</a></td>\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/monitorUserCluster.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Monitor{% endblock %}\n\n{% block panel_title %}NodeInfo for {{ muser }}{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/monitor/User/'>UsersInfo</a>\n  </li>\n  <li class='active'>\n      <strong>Clusters</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block content %}\n{% for cluster in clusters %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Cluster Name: {{ cluster }}</h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body\">\n\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t <th>Node ID</th>\n\t\t\t\t <th>Node Name</th>\n                                 <th>IP Address</th>\n                                 <th>Status</th>\n\t\t\t\t <th>Create Time</th>\n\t\t\t\t <th>detail</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for container in containers[cluster]['containers'] %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ container['containername'] }}</td>\n                                 <td>{{ container['ip'] }}</td>\n\n                                 {% if containers[cluster]['status'] == 'stopped' %}\n                                 <td><div class=\"label label-danger\">Stopped</div></td>\n                                 {% else %}\n                                 <td><div class=\"label label-primary\">Running</div></td>\n                                 {% endif %}\n\n                                 <td>xxxxx</td>\n                                 <td><a class=\"btn btn-info\" href='/monitor/Node/{{ container['containername'] }}/detail/'>Detail</a></td>\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n{% endfor %}\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/status.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Status{% endblock %}\n\n{% block panel_title %}Workspace VCluster Status{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n      <li class=\"active\">\n      <strong>VClusterStatus</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div class=\"row\">\n\t<div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Your Quotas</h3>\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n                              {% for quotaname in quotanames %}\n                                <th> {{ quotaname }} </th>\n                              {% endfor %}\n                             </tr>\n                             </thead>\n                             <tbody>\n                             <tr>\n                                {% for quotaname in quotanames %}\n                                  {% if quotaname == 'cpu' %}\n                                    {% if quotas['cpu'] == '1' %}\n                                    <th>{{ quotas['cpu'] }} Core</th>\n                                    {% else %}\n                                    <th>{{ quotas['cpu'] }} Cores</th>\n                                    {% endif %}\n                                  {% elif quotaname == 'memory' or quotaname == 'disk' %}\n                                    <th>{{ quotas[quotaname] }} MB</th>\n                                  {% elif quotaname == 'idletime' %}\n                                    <th>{{ quotas[quotaname] }} hours</th>\n                                  {% elif quotaname == 'input_rate_limit' or quotaname == 'output_rate_limit'%}\n                                    <th>{{ quotas[quotaname] }} kbps</th>\n                                  {% elif quotaname == 'data' %}\n                                    <th>{{ quotas[quotaname] }} GB</th>\n                                  {% else %}\n                                    <th>{{ quotas[quotaname] }}</th>\n                                  {% endif %}\n                                {% endfor %}\n\t\t\t                 </tr>\n                             </tbody>\n                         </table>\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n{% for master in allcontainers %}\n<div class=\"row\">\n\t<div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Total Network Statistics @ {{master.split(\"@\")[1]}}</h3>\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n                                <th>Total Bytes Sent</th>\n                                <th>Total Bytes Received</th>\n                                <th>Total Bytes Transefer</th>\n                                <th>Network Billings</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             <tr>\n                                <td id='{{master.split(\"@\")[1]}}_bytes_sent'>--</td>\n                                <td id='{{master.split(\"@\")[1]}}_bytes_recv'>--</td>\n                                <td id='{{master.split(\"@\")[1]}}_bytes_total'>--</td>\n                                <td id='{{master.split(\"@\")[1]}}_net_billings'>--</td>\n\t\t\t                       </tr>\n                             </tbody>\n                         </table>\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n{% for clustername, clusterinfo in allcontainers[master].items() %}\n\t  <div class=\"row\">\n\t     <div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n\t\t      <h3 class=\"box-title\">VCluster Name: {{ clustername }}<strong> @ {{master.split(\"@\")[1]}}</strong></h3>\n\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"box-body table-responsive\">\n\n                             {% for container in clusterinfo['containers'] %}\n                                  <div class=\"modal inmodal\" id='DetailModal_{{master.split(\"@\")[1]}}_{{ container['containername'] }}' tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                      <div class=\"modal-content animated fadeIn\">\n                                        <div class=\"modal-header\">\n                                          <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                          <h4 class=\"modal-title\">{{ container['containername'] }} Billing Detail</h4>\n                                          <small class=\"font-bold\">The Detail of the Billing In This Hour<br>Billing = cpu(s) / <span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_cpu_a'>?</span> + mem(MiB) / <span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_mem_b'>?</span> + Disk(MiB) / <span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_disk_c'>?</span> + Ports / <span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_port_d'>?</span>, <a target='_blank' title='How to figure out it?' href='https://unias.github.io/docklet/book/en/billing/billing.html'>See User Guide</a></small>\n                                        </div>\n                                        <div class=\"modal-body\">\n                                          <table class=\"table table-bordered\">\n                                            <thead>\n                                              <tr>\n                                                <th>Field</th>\n                                                <th>Usage</th>\n                                                <th>Cost</th>\n                                              </tr>\n                                            </thead>\n                                            <tbody>\n                                              <tr>\n                                                <td>Cpu</td>\n                                                <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_cpu_use'>--</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_cpu'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Memory</td>\n                                                <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_mem_use'>--</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_mem'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Disk</td>\n                                                <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_disk_use'>--</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_disk'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Ports</td>\n                                                <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_port_use'>--</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_port'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td colspan=\"2\">Total</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_total'>--</span></td>\n                                              </tr>\n                                            </tbody>\n                                          </table>\n                                          <div class=\"modal-footer\">\n                                            <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                          </div>\n                                        </div>\n                                      </div>\n                                    </div>\n                                  </div>\n\n                                  <div class=\"modal inmodal\" id='HistoryDetailModal_{{master.split(\"@\")[1]}}_{{ container['containername'] }}' tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                      <div class=\"modal-content animated fadeIn\">\n                                        <div class=\"modal-header\">\n                                          <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                          <h4 class=\"modal-title\">{{ container['containername'] }} Total Billing Detail</h4>\n                                        </div>\n                                        <div class=\"modal-body\">\n                                          <table class=\"table table-bordered\">\n                                            <thead>\n                                              <tr>\n                                                <th>Field</th>\n                                                <th>Cost</th>\n                                              </tr>\n                                            </thead>\n                                            <tbody>\n                                              <tr>\n                                                <td>Cpu</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_history_cpu'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Memory</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_history_mem'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Disk</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_history_disk'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Ports</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_history_port'>--</span></td>\n                                              </tr>\n                                              <tr>\n                                                <td>Total</td>\n                                                <td><span id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing_history_total'>--</span></td>\n                                              </tr>\n                                            </tbody>\n                                          </table>\n                                          <div class=\"modal-footer\">\n                                            <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                          </div>\n                                        </div>\n                                      </div>\n                                    </div>\n                                  </div>\n                             {% endfor %}\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n\t\t\t\t                 <th>Node ID</th>\n\t\t\t\t                 <th>Node Name</th>\n                                 <th>IP Address</th>\n                                 <th>Status</th>\n                                 <th>Running Time</th>\n\t\t\t\t                 <th>Cpu Usage</th>\n\t\t\t\t                 <th>Mem Usage</th>\n\t\t\t\t                 <th>Disk Usage</th>\n\t\t\t\t                 <th>Total Billing</th>\n\t\t\t\t                 <th>Billing This Running Hour</th>\n\t\t\t\t                 <th>Summary</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             {% for container in clusterinfo['containers'] %}\n                             <tr>\n                                 <td>{{ loop.index }}</td>\n                                 <td>{{ container['containername'] }}</td>\n                                 <td>{{ container['ip'] }}</td>\n\n                                 {% if clusterinfo['status'] == 'stopped' %}\n\t\t\t\t <td><div id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_state' class=\"label label-danger\">Stopped</div></td>\n                                 {% else %}\n                                 <td><div id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_state' class=\"label label-primary\">Running</div></td>\n                                 {% endif %}\n\t\t\t\t <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_time'>--</td>\n                                 <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_cpu'>--</td>\n                                 <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_mem'>--</td>\n                                 <td id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_disk'>--</td>\n                                 <td><a role=\"button\" id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billing' data-toggle=\"modal\" data-target='#HistoryDetailModal_{{master.split(\"@\")[1]}}_{{ container['containername'] }}'>--</a> <img src='/static/img/bean.png' /></td>\n                                 <td ><a role=\"button\" id='{{master.split(\"@\")[1]}}_{{ container['containername'] }}_billingthishour' data-toggle=\"modal\" data-target='#DetailModal_{{master.split(\"@\")[1]}}_{{ container['containername'] }}'>--</a> <img src='/static/img/bean.png' /></td>\n\t\t\t\t                         <td><a class=\"btn btn-info btn-xs\" href='/vclusters/{{master.split(\"@\")[0]}}/{{ clustername }}/{{ container['containername'] }}/'>Realtime</a></td>\n\t\t\t     </tr>\n                             {% endfor %}\n                             </tbody>\n                         </table>\n\n                     </div>\n                 </div>\n             </div>\n\t   </div>\n\n{% endfor %}\n{% endfor %}\n{% endblock %}\n\n{% block script_src %}\n\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n\n<script type='text/javascript'>\nfunction num2human(data)\n{\n   units=['','K','M','G','T'];\n   tempdata = data/1.0;\n   //return tempdata;\n   for(var i = 1; i < units.length; ++i)\n   {\n      if( tempdata / 1000.0 > 1)\n          tempdata = tempdata/1000.0;\n      else\n          return tempdata.toFixed(2) + units[i-1];\n   }\n   return tempdata.toFixed(2) + units[4];\n}\n   function update_net_stats(url,index)\n   {\n     $.post(url,{},function(data){\n       if(!data.net_stats.bytes_sent)\n       {\n         $(\"#\"+index+\"_bytes_sent\").html(\"--\");\n         $(\"#\"+index+\"_bytes_recv\").html(\"--\");\n         $(\"#\"+index+\"_bytes_total\").html(\"--\");\n         $(\"#\"+index+\"_net_billings\").html(\"--\");\n         return;\n       }\n          var bytes_sent = parseInt(data.net_stats.bytes_sent);\n          var bytes_recv = parseInt(data.net_stats.bytes_recv);\n        $(\"#\"+index+\"_bytes_sent\").html(num2human(bytes_sent)+\"B\");\n        $(\"#\"+index+\"_bytes_recv\").html(num2human(bytes_recv)+\"B\");\n        $(\"#\"+index+\"_bytes_total\").html(num2human(bytes_sent+bytes_recv)+\"B\");\n        $(\"#\"+index+\"_net_billings\").html(data.net_stats.net_billings);\n     },\"json\");\n   }\n   function update(url,index)\n   {\n\n       $.post(url+\"/info/\",{},function(data){\n\n            var diskuse = data.monitor.disk_use;\n\t\t       \tvar usedp = diskuse.percent;\n            var total = diskuse.total/1024.0/1024.0;\n            var used = diskuse.used/1024.0/1024.0;\n\t\t\t      var detail = \"(\"+used.toFixed(2)+\"MiB/\"+total.toFixed(2)+\"MiB)\";\n\t\t       \t$(\"#\"+index+\"_disk\").html(usedp+\"%<br/>\"+detail);\n\n            var total = parseInt(data.monitor.basic_info.RunningTime);\n            var hour = Math.floor(total / 3600);\n            var min = Math.floor(total % 3600 / 60);\n            var secs = Math.floor(total % 3600 % 60);\n\t    console.log(index,hour,min,secs);\n            $(\"#\"+index+\"_time\").html(hour+\"h \"+min+\"m \"+secs+\"s\")\n            $(\"#\"+index+\"_billing\").html(data.monitor.basic_info.billing)\n            $(\"#\"+index+\"_billingthishour\").html(data.monitor.basic_info.billing_this_hour.total)\n            $(\"#\"+index+\"_billing_total\").html(data.monitor.basic_info.billing_this_hour.total)\n            if (parseInt(data.monitor.basic_info.billing_this_hour.total) != 0) {\n              $(\"#\"+index+\"_billing_cpu\").html(data.monitor.basic_info.billing_this_hour.cpu)\n              $(\"#\"+index+\"_billing_cpu_use\").html(data.monitor.basic_info.billing_this_hour.cpu_time + \" s\")\n              $(\"#\"+index+\"_billing_mem\").html(data.monitor.basic_info.billing_this_hour.mem)\n              $(\"#\"+index+\"_billing_mem_use\").html(data.monitor.basic_info.billing_this_hour.mem_use + \" MiB\")\n              $(\"#\"+index+\"_billing_disk\").html(data.monitor.basic_info.billing_this_hour.disk)\n              $(\"#\"+index+\"_billing_disk_use\").html(data.monitor.basic_info.billing_this_hour.disk_use + \" MiB\")\n              $(\"#\"+index+\"_billing_port\").html(data.monitor.basic_info.billing_this_hour.port)\n              $(\"#\"+index+\"_billing_port_use\").html(data.monitor.basic_info.billing_this_hour.port_use)\n            }\n            if(!!data.monitor.basic_info.billing_history)\n            {\n              $(\"#\"+index+\"_billing_history_cpu\").html(data.monitor.basic_info.billing_history.cpu);\n              $(\"#\"+index+\"_billing_history_mem\").html(data.monitor.basic_info.billing_history.mem);\n              $(\"#\"+index+\"_billing_history_disk\").html(data.monitor.basic_info.billing_history.disk);\n              $(\"#\"+index+\"_billing_history_port\").html(data.monitor.basic_info.billing_history.port);\n            }\n            else\n            {\n              $(\"#\"+index+\"_billing_history_cpu\").html(0);\n              $(\"#\"+index+\"_billing_history_mem\").html(0);\n              $(\"#\"+index+\"_billing_history_disk\").html(0);\n              $(\"#\"+index+\"_billing_history_port\").html(0);\n            }\n            $(\"#\"+index+\"_billing_history_total\").html(data.monitor.basic_info.billing)\n\n            $(\"#\"+index+\"_billing_cpu_a\").html(data.monitor.basic_info.a_cpu)\n            $(\"#\"+index+\"_billing_mem_b\").html(data.monitor.basic_info.b_mem)\n            $(\"#\"+index+\"_billing_disk_c\").html(data.monitor.basic_info.c_disk)\n            $(\"#\"+index+\"_billing_port_d\").html(data.monitor.basic_info.d_port)\n\n\t\t    var state = data.monitor.basic_info.State;\n\t\t    if(state == 'RUNNING')\n       \t\t{\n           \t    var tmp = $(\"#\"+index+\"_state\");\n                    tmp.removeClass();\n\t            tmp.addClass(\"label label-primary\");\n\t   \t    tmp.html(\"Running\");\n\t\t    $(\"#\"+index+\"_pid\").html(data.monitor.basic_info.PID);\n\t\t    $(\"#\"+index+\"_ip\").html(data.monitor.basic_info.IP);\n       \t\t}\n       \t\telse if(state == 'STOPPED')\n       \t\t{\n           \t    var tmp = $(\"#\"+index+\"_state\");\n           \t    tmp.removeClass();\n\t   \t    tmp.addClass(\"label label-danger\");\n\t   \t    tmp.html(\"Stopped\");\n\t\t    $(\"#\"+index+\"_pid\").html('--');\n\t\t    $(\"#\"+index+\"_ip\").html('--');\n\t\t    $(\"#\"+index+\"_cpu\").html('--');\n\t\t    $(\"#\"+index+\"_mem\").html('--');\n\t\t    return;\n       \t\t}\n\n\t\t       \tvar usedp = data.monitor.cpu_use.usedp;\n                var quota = data.monitor.cpu_use.quota.cpu;\n                var quotaout = \"(\"+quota;\n                if(quota == 1)\n                    quotaout += \" Core)\";\n                else\n                    quotaout += \" Cores)\";\n\t\t       \t$(\"#\"+index+\"_cpu\").html((usedp/0.01).toFixed(2)+\"%<br/>\"+quotaout);\n\n\t\t       \tvar usedp = data.monitor.mem_use.usedp;\n\t\t\tvar unit = data.monitor.mem_use.unit;\n\t\t\tvar quota = data.monitor.mem_use.quota.memory/1024.0;\n\t\t\tvar val = data.monitor.mem_use.val;\n\t\t\tvar out = \"(\"+val+unit+\"/\"+quota.toFixed(2)+\"MiB)\";\n\t\t       \t$(\"#\"+index+\"_mem\").html((usedp/0.01).toFixed(2)+\"%<br/>\"+out);\n\n       \t\t},\"json\");\n   }\n\n   function updateAll()\n   {\n        var host = window.location.host;\n\t{% for master in allcontainers %}\n\turl = \"//\" + host + \"/monitor/\" + '{{master.split(\"@\")[0]}}' + \"/user/net_stats/\";\n  update_net_stats(url,'{{master.split(\"@\")[1]}}')\n \t{% for clustername, clusterinfo in allcontainers[master].items() %}\n\t{% for container in clusterinfo['containers'] %}\n\t//url = url0 + '{{ container['containername'] }}';\n\turl = \"//\" + host + \"/monitor/\" + '{{master.split(\"@\")[0]}}' + \"/vnodes/\" + '{{container[\"containername\"]}}' ;\n\tupdate(url,'{{master.split(\"@\")[1]}}_{{ container['containername'] }}');\n\t{% endfor %}\n\t{% endfor %}\n\t{% endfor %}\n   }\n   updateAll()\n   setInterval(updateAll,5000);\n\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/monitor/statusRealtime.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Node Summary{% endblock %}\n\n{% block panel_title %}Summary for <div id='node_name'>{{ node_name }}</div>{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li>\n      <a href='/vclusters/'>VClusterStatus</a>\n  </li>\n  <li class=\"active\">\n      <strong>Summary</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"/static/dist/css/flotconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block content %}\n<div id='masterip' style=\"display:none\">{{masterip.split(\"@\")[0]}}</div>\n<div class=\"row\">\n<div class=\"col-md-12\">\n<div class=\"box box-info\">\n     <div class=\"box-header with-border\">\n       <h3 class=\"box-title\">Current Status of {{ container['Name'] }}</h3>\n\n       <div class=\"box-tools pull-right\">\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n         </button>\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n       </div>\n     </div>\n            <div class=\"ibox-body table-responsive\">\n                <table class=\"table table-bordered\">\n                    <thead>\n                    <tr>\n                        <th>State</th>\n                        <th>IP Address</th>\n                        <th>Running Time</th>\n                        <th>CPU Usage</th>\n                        <th>Mem Usage</th>\n                        <th>Disk Usage</th>\n                        <th>Total Billing</th>\n                        <th>Billing This Running Hour</th>\n                    </tr>\n                    </thead>\n                    <tbody>\n                      <tr>\n                        {% if container['State'] == 'STOPPED' %}\n                        <td id='con_state'><div class=\"label label-danger\">Stopped</div></td>\n                        <td id='con_ip'>--</td>\n                        {% else %}\n                        <td id='con_state'><div class=\"label label-primary\">Running</div></td>\n                        <td id='con_ip'>{{ container['IP'] }}</td>\n                        {% endif %}\n                        <td id='con_time'>{{ container['RunningTime'] }}s</td>\n                        <td id='con_cpu'>--</td>\n                        <td id='con_mem'>--</td>\n                        <td id='con_disk'>--</td>\n                        <td id='con_billing'>--</td>\n                        <td id='con_billingthishour'>--</td>\n                      </tr>\n                    </tbody>\n                </table>\n\n            </div>\n        </div>\n    </div>\n</div>\n<div class=\"row\">\n\t<div class=\"col-md-12\">\n         <div class=\"box box-info\">\n              <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Network Statistics</h3>\n                <div class=\"box-tools pull-right\">\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                  </button>\n                  <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                </div>\n              </div>\n                     <div class=\"ibox-body table-responsive\">\n\n                         <table class=\"table table-bordered\">\n                             <thead>\n                             <tr>\n                                <th colspan=\"5\" style=\"text-align:center\">Ingress</th>\n                                <th colspan=\"5\" style=\"text-align:center\">Egress</th>\n                             </tr>\n                             <tr>\n                                <th>Rate</th>\n                                <th>Received Bytes</th>\n                                <th>Received Packets</th>\n                                <th>Error Packets</th>\n                                <th>Dropped Packets</th>\n                                <th>Rate</th>\n                                <th>Sent Bytes</th>\n                                <th>Sent Packets</th>\n                                <th>Error Packets</th>\n                                <th>Dropped Packets</th>\n                             </tr>\n                             </thead>\n                             <tbody>\n                             <tr>\n                               <td id='net_in_rate'>--</td>\n                               <td id='net_in_bytes'>--</td>\n                               <td id='net_in_packs'>--</td>\n                               <td id='net_in_err'>--</td>\n                               <td id='net_in_drop'>--</td>\n                               <td id='net_out_rate'>--</td>\n                               <td id='net_out_bytes'>--</td>\n                               <td id='net_out_packs'>--</td>\n                               <td id='net_out_err'>--</td>\n                               <td id='net_out_drop'>--</td>\n\t\t\t                       </tr>\n                             </tbody>\n                         </table>\n                     </div>\n        </div>\n    </div>\n</div>\n<div class=\"row\">\n    <div class=\"col-lg-6\">\n      <div class=\"box box-info\">\n           <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">Memory Used(%):</h3>\n\n             <div class=\"box-tools pull-right\">\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n               </button>\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n             </div>\n           </div>\n            <div class=\"box-body\">\n\n                <div class=\"flot-chart\">\n                    <div class=\"flot-chart-content\" id=\"mem-chart\"></div>\n                </div>\n            </div>\n        </div>\n    </div>\n\t\t<div class=\"col-lg-6\">\n      <div class=\"box box-info\">\n           <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">CPU Used(%):</h3>\n\n             <div class=\"box-tools pull-right\">\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n               </button>\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n             </div>\n           </div>\n            <div class=\"box-body\">\n\n                <div class=\"flot-chart\">\n                    <div class=\"flot-chart-content\" id=\"cpu-chart\"></div>\n                </div>\n            </div>\n        </div>\n     </div>\n  </div>\n  <div class=\"row\">\n      <div class=\"col-lg-6\">\n        <div class=\"box box-info\">\n             <div class=\"box-header with-border\">\n               <h3 class=\"box-title\">Ingress Rate(kbps):</h3>\n\n               <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n               </div>\n             </div>\n              <div class=\"box-body\">\n\n                  <div class=\"flot-chart\">\n                      <div class=\"flot-chart-content\" id=\"ingress-chart\"></div>\n                  </div>\n              </div>\n          </div>\n      </div>\n  \t\t<div class=\"col-lg-6\">\n        <div class=\"box box-info\">\n             <div class=\"box-header with-border\">\n               <h3 class=\"box-title\">Egress Rate(kbps):</h3>\n\n               <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n               </div>\n             </div>\n              <div class=\"box-body\">\n\n                  <div class=\"flot-chart\">\n                      <div class=\"flot-chart-content\" id=\"egress-chart\"></div>\n                  </div>\n              </div>\n          </div>\n       </div>\n    </div>\n{% endblock %}\n\n{% block script_src %}\n\n<!-- Flot -->\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.js\"></script>\n<script src=\"//cdn.bootcss.com/flot.tooltip/0.8.6/jquery.flot.tooltip.min.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.resize.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.pie.min.js\"></script>\n<script src=\"//cdn.bootcss.com/flot/0.8.3/jquery.flot.time.min.js\"></script>\n\n<script src=\"/static/js/plot_monitor.js\"></script>\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/notification.html",
    "content": "{% extends \"base_AdminLTE.html\" %}\n{% block title %}Docklet | Notification{% endblock %}\n\n{% block panel_title %}Notifications{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Notifications</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{##}\n{% block content %}\n<div class=\"row\">\n    <div class=\"col-lg-12\">\n        <div class=\"box box-info\">\n            <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Notifications</h3>\n\n                <div class=\"box-tools pull-right\">\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\">\n                        <i class=\"fa fa-minus\"></i>\n                    </button>\n                    <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\">\n                        <i class=\"fa fa-times\"></i>\n                    </button>\n                </div>\n            </div>\n\n            <div class=\"box-body\" style=\"white-space:normal;word-break:break-all;word-wrap:break-word;\">\n                <p>\n                    <a href=\"/notification/create/\">\n                        <button type=\"button\" class=\"btn btn-primary\"><i class=\"fa fa-plus\"></i> Add new notification</button>\n                    </a>\n                </p>\n                <div class=\"table table-responsive\">\n                    <table id=\"notificationTable\" class=\"table table-striped table-bordered\">\n                        <thead>\n                            <tr>\n                                <th>ID</th>\n                                <th>Title</th>\n                                <th>Content</th>\n                                <th>Groups</th>\n                                <th>Create Date</th>\n                                <th>Status</th>\n                                <th>Command</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                        {% for notify in notifications %}\n                            <tr>\n                                <td>{{ notify['id'] }}</td>\n                                <td>{{ notify['title'] }}</td>\n                                <td>{{ notify['content']|truncate(30) }}</td>\n                                <td>\n                                    {% for group_name in notify['groups'] %}<code>{{ group_name }}</code>&nbsp;&nbsp;{% endfor %}\n                                </td>\n                                <td>{{ notify['create_date'] }}</td>\n                                <td>{{ notify['status'] }}</td>\n                                <td>\n                                    <a class=\"btn btn-xs btn-success\" data-toggle=\"modal\" data-target=\"#detailModal_{{ notify['id'] }}\"><i class=\"fa fa-plus\"></i> details</a>&nbsp;\n                                    <a class=\"btn btn-xs btn-info\" data-toggle=\"modal\" data-target=\"#editModal_{{ notify['id'] }}\"><i class=\"fa fa-pencil\"></i> edit</a>&nbsp;\n                                    <a class=\"btn btn-xs btn-danger\" data-toggle=\"modal\" data-target=\"#deleteModal_{{ notify['id'] }}\"><i class=\"fa fa-times\"></i> delete</a>\n                                </td>\n                            <!-- Modal Dialog -->\n                                <div class=\"modal inmodal\" id=\"detailModal_{{ notify['id'] }}\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                        <div class=\"modal-content\">\n                                            <div class=\"modal-header\">\n                                                <button type=\"button\" class=\"close\" data-dismiss=\"modal\">\n                                                    <span aria-hidden=\"true\">&times;</span>\n                                                    <span class=\"sr-only\">Close</span>\n                                                </button>\n                                                <i class=\"fa fa-envelope modal-icon\"></i>\n                                                <h4 class=\"modal-title\">Notification Details</h4>\n                                                <small class=\"font-bold\">Show the details of the notification</small>\n                                            </div>\n                                            <div class=\"modal-body\">\n                                                <h4><i class=\"fa fa-star\"></i> Title</h4>\n                                                <p>{{ notify['title'] }}</p>\n                                                <br>\n                                                <h4><i class=\"fa fa-star\"></i> Content</h4>\n                                                <p>{{ notify['content'] }}</p>\n                                            </div>\n                                            <div class=\"modal-footer\">\n                                                <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"modal inmodal\" id=\"editModal_{{ notify['id'] }}\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                        <div class=\"modal-content\">\n                                            <div class=\"modal-header\">\n                                                <button type=\"button\" class=\"close\" data-dismiss=\"modal\">\n                                                    <span aria-hidden=\"true\">&times;</span>\n                                                    <span class=\"sr-only\">Close</span>\n                                                </button>\n                                                <i class=\"fa fa-envelope modal-icon\"></i>\n                                                <h4 class=\"modal-title\">Edit Notification</h4>\n                                                <small class=\"font-bold\">Edit the details of the notification</small>\n                                            </div>\n                                            <div class=\"modal-body\">\n                                                <form id=\"modifyNotificationForm_{{ notify['id'] }}\" action=\"/notification/modify/\" method=\"post\">\n                                                    <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                                    <div class=\"form-group\">\n                                                        <label>Title</label>\n                                                        <input type=\"text\" class=\"form-control\" name=\"title\" value=\"{{ notify['title'] }}\">\n                                                    </div>\n                                                    <div class=\"form-group\">\n                                                        <label>Content</label>\n                                                        <textarea class=\"form-control\" name=\"content\">{{ notify['content'] }}</textarea>\n                                                    </div>\n                                                    <div class=\"form-group\" style=\"margin-bottom: 0\">\n                                                        <label>Groups</label>\n                                                        <input type=\"text\" class=\"form-control\" name=\"notify_id\" style=\"display: none\" value=\"{{ notify['id'] }}\">\n                                                    </div>\n                                                    <div class=\"form-group\">\n                                                        <div class=\"checkbox col-sm-10\">\n                                                            <label style=\"display: none\"><input type=\"checkbox\" checked name=\"groups\" value=\"none\" style=\"display: none\"></label>\n                                                            {% if 'all' in notify['groups'] %}\n                                                                <label><input type=\"checkbox\" checked name=\"groups\" value=\"all\">all&nbsp;&nbsp;</label>\n                                                            {% else %}\n                                                                <label><input type=\"checkbox\" name=\"groups\" value=\"all\">all&nbsp;&nbsp;</label>\n                                                            {% endif %}\n                                                            {% for group_name in groups %}\n                                                                {% if group_name in notify['groups'] %}\n                                                                    <label><input type=\"checkbox\" checked name=\"groups\" value=\"{{ group_name }}\">{{ group_name }}&nbsp;&nbsp;</label>\n                                                                {% else %}\n                                                                    <label><input type=\"checkbox\" name=\"groups\" value=\"{{ group_name }}\">{{ group_name }}&nbsp;&nbsp;</label>\n                                                                {% endif %}\n                                                            {% endfor %}\n                                                        </div>\n                                                    </div>\n                                                    <div class=\"row\"></div>\n                                                    <div class=\"form-group\">\n                                                        <h5>\n                                                            <b>Status</b>\n                                                        </h5>\n                                                        <div class=\"radio\">\n                                                            {% if 'open' == notify['status'] %}\n                                                                <label><input type=\"radio\" name=\"status\" checked value=\"open\">open</label>\n                                                                <label><input type=\"radio\" name=\"status\" value=\"closed\">closed</label>\n                                                            {% else %}\n                                                                <label><input type=\"radio\" name=\"status\" value=\"open\">open</label>\n                                                                <label><input type=\"radio\" name=\"status\" checked value=\"closed\">closed</label>\n                                                            {% endif %}\n                                                        </div>\n                                                    </div>\n                                                    <div class=\"form-group\">\n                                                        <h5>\n                                                            <b>Also send email</b>\n                                                        </h5>\n                                                        <input type=\"checkbox\" name=\"sendMail\" value=\"true\">\n                                                    </div>\n                                                </form>\n                                            </div>\n                                            <div class=\"modal-footer\">\n                                                <button type=\"button\" class=\"btn btn-primary\" onClick=\"sendModifyNotification({{ notify['id'] }});\">Save</button>\n                                                <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                                <div class=\"modal inmodal\" id=\"deleteModal_{{ notify['id'] }}\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                        <div class=\"modal-content\">\n                                            <div class=\"modal-header\">\n                                                <button type=\"button\" class=\"close\" data-dismiss=\"modal\">\n                                                    <span aria-hidden=\"true\">&times;</span>\n                                                    <span class=\"sr-only\">Close</span>\n                                                </button>\n                                                <h4 class=\"modal-title\">Delete Notification </h4>\n                                                <small class=\"font-bold\">Delete this notification</small>\n                                            </div>\n                                            <div class=\"modal-body\">\n                                                <strong class=\"text-center\">Are you sure to do this?</strong>\n                                                <form id=\"deleteNotificationForm_{{ notify['id'] }}\" style=\"display: none\" action=\"/notification/delete/\" method=\"post\">\n                                                    <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                                    <input type=\"text\" class=\"form-control\" name=\"notify_id\" style=\"display: none\" value=\"{{ notify['id'] }}\">\n                                                </form>\n                                            </div>\n                                            <div class=\"modal-footer\">\n                                                <button type=\"button\" class=\"btn btn-danger\" onClick=\"sendDeleteNotification({{ notify['id'] }});\">Delete</button>\n                                                <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Cancel</button>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                            </tr>\n                        {% endfor %}\n                        </tbody>\n                    </table>\n                </div>\n\n            </div>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n    <script type=\"text/javascript\">\n        function sendModifyNotification(notifyId) {\n            $('#modifyNotificationForm_'+notifyId).submit();\n        }\n        function sendDeleteNotification(notifyId) {\n            $('#deleteNotificationForm_'+notifyId).submit();\n        }\n    </script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/notification_info.html",
    "content": "{% extends \"base_AdminLTE.html\" %}\n{% block title %}Docklet | Notification{% endblock %}\n\n{% block panel_title %}Notifications{% endblock %}\n\n{% block panel_list %}\n    <ol class=\"breadcrumb\">\n        <li>\n            <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n        </li>\n        <li class=\"active\">\n            <strong>Notifications</strong>\n        </li>\n    </ol>\n{% endblock %}\n\n{##}\n{% block content %}\n    {% for notify in notifies %}\n        <div class=\"row\">\n            <div class=\"col-lg-12\">\n                <div class=\"box box-info\">\n                    <div class=\"box-header with-border\">\n                        <h3 class=\"box-title\">{{ notify['title'] }}</h3>\n\n                        <div class=\"box-tools pull-right\">\n                            <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\">\n                                <i class=\"fa fa-minus\"></i>\n                            </button>\n                            <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\">\n                                <i class=\"fa fa-times\"></i>\n                            </button>\n                        </div>\n                    </div>\n\n                    <div class=\"box-body\" style=\"white-space:normal;word-break:break-all;word-wrap:break-word;\">\n                        {{ notify['content'] }}\n                    </div>\n                    <div class=\"box-footer\">\n                        <small>{{ notify['create_date'] }}</small>\n                    </div>\n                </div>\n            </div>\n        </div>\n    {% endfor %}\n{% endblock %}\n\n{% block script_src %}\n\n{% endblock %}"
  },
  {
    "path": "web/templates/opfailed.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Failed{% endblock %}\n\n{% block panel_title %}Failed{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <strong><i class=\"fa fa-dashboard\"></i>Home</strong>\n  </li>\n      <li class=\"active\">\n      <strong>Failed</strong>\n  </li>\n</ol>\n{% endblock %}\n{% block content %}\n<div class=\"ibox-content text-center p-md\">\n\t<h1>Failed</h1>\n\t<br/>\n\t<br/>\n\t<a href=\"/dashboard/\"><span class=\"btn btn-w-m btn-success\">Click Here Back To The Workspace</span></a>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/opsuccess.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Success{% endblock %}\n\n{% block panel_title %}Success{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <strong><i class=\"fa fa-dashboard\"></i>Home</strong>\n  </li>\n      <li class=\"active\">\n      <strong>Success</strong>\n  </li>\n</ol>\n{% endblock %}\n{% block content %}\n<div class=\"ibox-body text-center p-md\">\n\t<h1>SUCCESS</h1>\n\t<br/>\n\t<br/>\n\t<pre>{{message}}</pre>\n\t<br/>\n\t<br/>\n\t<a href=\"/dashboard/\"><span class=\"btn btn-w-m btn-success\">Click Here Back To The Workspace</span></a>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/register.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>Docklet | Login</title>\n  <!-- Tell the browser to be responsive to screen width -->\n  <meta content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" name=\"viewport\">\n  <!-- Bootstrap 3.3.5 -->\n  <link href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\n  <!-- Font Awesome -->\n  <link href=\"//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n  <!-- Ionicons -->\n  <link href=\"//cdn.bootcss.com/ionicons/2.0.1/css/ionicons.min.css\" rel=\"stylesheet\">\n  <!-- Theme style -->\n  <link rel=\"stylesheet\" href=\"/static/dist/css/AdminLTE.min.css\">\n\n  <link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\n\n\n</head>\n<body class=\"hold-transition login-page\">\n<div class=\"login-box\">\n  <div class=\"login-logo\">\n    <img src=\"/static/img/logo.png\" class=\"logo-name\" height=\"50%\" width=\"50%\">\n    <!--a href=\"/\"><b>Docklet</b></a-->\n  </div>\n  <!-- /.login-logo -->\n  <div class=\"login-box-body\">\n    <p class=\"login-box-msg\">An easy and quick way to launch your DISTRIBUTED applications!</p>\n            <form class=\"m-t\" role=\"form\" action=\"\" id=\"activateForm\" method=\"POST\">\n                <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"username\" required=\"\" name=\"username\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"password\" required=\"\" name=\"password\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"repeat password\" required=\"\" name=\"password2\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"email\" class=\"form-control\" placeholder=\"E-mail\" required=\"\" name=\"email\">\n                </div>\n                <div class=\"form-group\">\n                    <textarea  class=\"form-control\"  name=\"description\" form=\"activateForm\" id=\"mDescription\">\n                    </textarea>\n                </div>\n                <div class=\"row\">\n                  <div class=\"col-xs-12\">\n                    <button type=\"submit\" class=\"btn btn-primary btn-block btn-flat\">Register</button>\n                  </div>\n                </div>\n                <!--p class=\"text-muted text-center\"><small>Do not have an account?</small></p-->\n                <!--a class=\"btn btn-sm btn-white btn-block\" href=\"register.html\">Create an account</a-->\n            </form>\n          </div>\n          <!-- /.login-box-body -->\n        </div>\n        <!-- /.login-box -->\n\n        <!-- jQuery 2.2.1 -->\n        <script src=\"//cdn.bootcss.com/jquery/2.2.1/jquery.min.js\"></script>\n        <!-- Bootstrap 3.3.5 -->\n        <script src=\"//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\n\n        </body>\n        </html>\n"
  },
  {
    "path": "web/templates/saveconfirm.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Confirm{% endblock %}\n\n{% block panel_title %}Confirm{% endblock %}\n\n{% block css_src %}\n.hide { display:none; }\n{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <strong><i class=\"fa fa-dashboard\"></i>Home</strong>\n  </li>\n      <li class=\"active\">\n      <strong>Confirm</strong>\n  </li>\n</ol>\n{% endblock %}\n\n\n{% block content %}\n<div class=\"box-body text-center p-md\">\n\t<form action=\"/workspace/{{masterip}}/save/{{ clustername }}/{{ containername }}/force/\" method=\"POST\">\n  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">  \n\t<label>Image:</label>\n\t<input type=\"text\" name=\"ImageName\" id=\"ImageName\" readonly=\"true\" value=\"{{ image }}\"/>\n\t<label> exists, are you sure to overwrite it?</label>\n\t<div class=\"hide\"><input type=\"text\" name=\"description\" id=\"description\" readonly=\"true\" value=\"{{ description }}\"/></div>\n\t<br/>\n\t<button type=\"submit\" class=\"btn btn-warning\">Yes</button>\n\t<a href=\"/config/\"><button type=\"button\" class=\"btn btn-success\">No</button></a>\n</form>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/settings.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\n{% block title %}Docklet | Settings{% endblock %}\n\n{% block panel_title %}Settings{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>Settings</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n{% endblock %}\n\n\n{% block content %}\n<div class=\"row\">\n <div class=\"col-md-12\">\n     <div class=\"box box-info\">\n         <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">Quota</h3>\n\n             <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i>\n                 </button>\n             </div>\n         </div>\n         <div class=\"box-body\">\n             <button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#AddGroupModal\"><i class=\"fa fa-plus\"></i> Add Quota Group</button>\n           <div class=\"modal inmodal\" id=\"AddGroupModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n               <div class=\"modal-dialog\">\n               <div class=\"modal-content animated fadeIn\">\n                       <div class=\"modal-header\">\n                           <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                           <i class=\"fa fa-laptop modal-icon\"></i>\n                           <h4 class=\"modal-title\">Add Group</h4>\n                           <small class=\"font-bold\">Add a group to Docklet</small>\n                       </div>\n                       <div class=\"modal-body\">\n\n                            <form action=\"/group/add/\" method=\"POST\" id=\"addGroupForm\">\n                              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                <div class=\"form-group\">\n                                    <label>Name</label>\n                                    <input type=\"text\" placeholder=\"Enter Name\" class=\"form-control\" name=\"groupname\"/>\n                                </div>\n                                {% for quota in quotas %}\n                                <div class=\"form-group\">\n                                    <label>{{ quota['name'] }}</label>\n                                    {% if quota['name'] == 'vnode' %}\n                                    <small class=\"font-bold\"> Only 2^n - 3 is legal. Any other value will be reset to the nearest 2^n - 3.</small>\n                                    {% endif %}\n                                    <input type=\"text\" class=\"form-control\" name={{ quota['name'] }} placeholder=\"{{quota['hint']}}\" />\n                                </div>\n                                {% endfor %}\n                            </form>\n\n                       </div>\n                       <div class=\"modal-footer\">\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                           <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendAddGroup();\">Submit</button>\n                       </div>\n                   </div>\n               </div>\n           </div>\n             <button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#AddQuotaModal\"><i class=\"fa fa-plus\"></i> Add Quota</button>\n           <div class=\"modal inmodal\" id=\"AddQuotaModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n               <div class=\"modal-dialog\">\n               <div class=\"modal-content animated fadeIn\">\n                       <div class=\"modal-header\">\n                           <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                           <i class=\"fa fa-laptop modal-icon\"></i>\n                           <h4 class=\"modal-title\">Add Quota</h4>\n                           <small class=\"font-bold\">Add a quota to Docklet</small>\n                       </div>\n                       <div class=\"modal-body\">\n\n                            <form action=\"/quota/add/\" method=\"POST\" id=\"addQuotaForm\">\n                              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                <div class=\"form-group\">\n                                    <label>Name</label>\n                                    <input type=\"text\" placeholder=\"Enter Name\" class=\"form-control\" name=\"quotaname\"/>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label>Default Value</label>\n                                    <input type=\"text\" placeholder=\"Enter Default Value\" class=\"form-control\" name=\"default_value\"/>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label>Hint</label>\n                                    <input type=\"text\" placeholder=\"Enter Hint\" class=\"form-control\" name=\"hint\"/>\n                                </div>\n                            </form>\n\n                       </div>\n                       <div class=\"modal-footer\">\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                           <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendAddQuota();\">Submit</button>\n                       </div>\n                   </div>\n               </div>\n           </div>\n             <button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#ChangeDefaultModal\"><i class=\"fa fa-plus\"></i> Change Default</button>\n           <div class=\"modal inmodal\" id=\"ChangeDefaultModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n               <div class=\"modal-dialog\">\n               <div class=\"modal-content animated fadeIn\">\n                       <div class=\"modal-header\">\n                           <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                           <i class=\"fa fa-laptop modal-icon\"></i>\n                           <h4 class=\"modal-title\">Change Default</h4>\n                           <small class=\"font-bold\">Change Default Quota Group</small>\n                       </div>\n                       <div class=\"modal-body\">\n                            <form action=\"/quota/chdefault/\" method=\"POST\" id=\"chDefaultForm\">\n                              <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                <div class=\"form-group\">\n                                    <label>Default Group</label>\n                                    <select class=\"form-control\" name=\"defaultgroup\" value={{ defaultgroup }} />\n                                        {% for group in groups %}\n                                        <option>{{ group['name'] }}</option>\n                                        {% endfor %}\n                                    </select>\n                                </div>\n                            </form>\n                       </div>\n                       <div class=\"modal-footer\">\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                           <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendChDefault();\">Submit</button>\n                       </div>\n                   </div>\n               </div>\n           </div>\n        <div class=\"table table-responsive\">\n         <table id=\"myGroupTable\" class=\"table table-striped table-bordered\">\n           <thead>\n               <tr>\n                   <th>Name</th>\n                   {% for quota in quotas %}\n                   <th> {{ quota['name'] }} </th>\n                   {% endfor %}\n                   <th>Command</th>\n               </tr>\n           </thead>\n           <tbody>\n               {% for group in groups %}\n               <tr>\n                   <th>{{ group['name'] }}</th>\n                    {% for quota in quotas %}\n                    <th> {{ group['quotas'][quota['name']] }} </th>\n                    {% endfor %}\n                    <th><a class=\"btn btn-xs btn-info\" data-toggle=\"modal\" data-target=\"#ModifyGroupModal_{{ group['name'] }}\">Edit</a>&nbsp;\n                    {% if group['name'] in [ \"root\", \"primary\", \"admin\", \"foundation\" ] %}\n                        <a class=\"btn btn-xs btn-default\" href=\"javascript:void(0)\">Delete</a>&nbsp;\n                    {% else %}\n                        <a class=\"btn btn-xs btn-danger\" href=\"/group/delete/{{group['name']}}\">Delete</a>&nbsp;\n                    {% endif %}\n                    {% if group['name'] == defaultgroup %}\n                        <span class=\"glyphicon glyphicon-ok\"></span>\n                    {% endif %}\n                    </th>\n                    <div class=\"modal inmodal\" id=\"ModifyGroupModal_{{ group['name'] }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                        <div class=\"modal-dialog\">\n                        <div class=\"modal-content animated fadeIn\">\n                                <div class=\"modal-header\">\n                                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4 class=\"modal-title\">Modify Group</h4>\n                                    <small class=\"font-bold\">Modify a group in Docklet</small>\n                                </div>\n                                <form action=\"/group/modify/{{group['name']}}/\" method=\"POST\" >\n                                  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                    <div class=\"modal-body\">\n                                      <div class=\"form-group\">\n                                          <label>Name</label>\n                                          <input type=\"text\" placeholder=\"Enter Name\" class=\"form-control\" name=\"groupname\" readonly=\"true\" value={{ group['name'] }} />\n                                      </div>\n                                      {% for quota in quotas %}\n                                      <div class=\"form-group\">\n                                          <label> {{ quota['name'] }}</label>\n                                          {% if quota['name'] == 'vnode' %}\n                                          <small class=\"font-bold\"> Only 2^n - 3 are legal. Any other value will be reset to the nearest 2^n - 3.</small>\n                                          {% endif %}\n                                          <input type=\"text\" placeholder=\"{{ quota['hint'] }}\" class=\"form-control\" name={{ quota['name'] }} value={{ group['quotas'][quota['name']] }} />\n                                      </div>\n                                      {% endfor %}\n                                      <small class=\"font-bold\"> *: The update of <i>vnode</i>, <i>input_rate_limit</i> and <i>output_rate_limit</i> will take effect when a user deletes all his/her workpsaces.</small>\n                                    </div>\n                                    <div class=\"modal-footer\">\n                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                        <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                                    </div>\n                                </form>\n                        </div>\n                        </div>\n                    </div>\n               </tr>\n               {% endfor %}\n           </tbody>\n         </table>\n       </div>\n       </div>\n     </div>\n </div>\n    <div class=\"col-md-12\">\n        <div class=\"box box-info\">\n            <div class=\"box-header with-border\">\n                <h3 class=\"box-title\">Update Base Image</h3>\n                    <div class=\"box-tools pull-right\">\n                        <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i></button>\n                        <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n                    </div>\n            </div>\n            <div class=\"box-body\">\n                <div class=\"table table-responsive\">\n                    <table id=\"imageTable\" class=\"table table-striped table-bordered\">\n                        <thead>\n                            <tr>\n                                <th>ImageName</th>\n                                <th>CreateTime</th>\n                                <th>Description</th>\n                                <th>Operation</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            {% for image in root_image %}\n                            <tr>\n                                <td>{{image['name']}}</td>\n                                <td>{{image['time']}}</td>\n                                <td><a href=\"/image/description/{{image['name']}}_root_private/\" target=\"_blank\">{{image['description']}}</a></td>\n                                <td><button type=\"button\" class=\"btn btn-xs btn-success\" data-toggle=\"modal\" data-target=\"#Update_{{image['name']}}\">Update</button>\n                                <div class=\"modal inmodal\" id=\"Update_{{image['name']}}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                                    <div class=\"modal-dialog\">\n                                        <div class=\"modal-content animated fadeIn\">\n                                            <div class=\"modal-header\">\n                                                <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                                <i class=\"fa fa-save modal-icon\"></i>\n                                                <h4 class=\"modal-title\">Update Package Image</h4>\n                                                <small class=\"font-bold\">Update Package Image From Chosen Image</small>\n                                            </div>\n                                            <div class=\"modal-body\">\n                                                <strong>Warning: This operation will update the package image. Maybe it will cause some error and then the package image will be destroyed. Please make sure you have the backup of package image.</strong>\n                                            </div>\n                                            <div class=\"modal-footer\">\n                                                <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                                <a href=\"/image/0.0.0.0/updatebase/{{image['name']}}/\"><button type=\"button\" class=\"btn btn-success\">Update</button></a>\n                                            </div>\n                                        </div>\n                                    </div>\n                                </div>\n                            </tr>\n                            {% endfor %}\n                        </tbody>\n                     </table>\n                 </div>\n             </div>\n         </div>\n     </div>\n <div class=\"col-md-12\">\n     <div class=\"box box-info\">\n         <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">Container Default Setting</h3>\n\n             <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i>\n                 </button>\n             </div>\n         </div>\n         <div class=\"box-body\">\n             <form id=\"chlxcsetting\" class=\"form-horizontal\" action=\"/quota/chlxcsetting/\" method=\"POST\">\n               <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                 <div class=\"form-group\">\n                     <label class=\"control-label col-sm-2\">CPU</label>\n                     <div class=\"col-sm-8\"><input type=\"number\" class=\"form-control\" name=\"lxcCpu\" id=\"lxcCpu\" value={{lxcsetting['cpu']}} />CORE</div>\n                 </div>\n                 <div class=\"form-group\">\n                     <label class=\"control-label col-sm-2\">MEMORY</label>\n                     <div class=\"col-sm-8\"><input type=\"number\" class=\"form-control\" name=\"lxcMemory\" id=\"lxcMemory\" value={{lxcsetting['memory']}} />MB</div>\n                 </div>\n                 <div class=\"form-group\">\n                     <label class=\"control-label col-sm-2\">DISK</label>\n                     <div class=\"col-sm-8\"><input type=\"number\" class=\"form-control\" name=\"lxcDisk\" id=\"lxcDisk\" value={{lxcsetting['disk']}} />MB</div>\n                 </div>\n                 <div class=\"form-group\">\n                    <div class=\"col-sm-4 col-sm-offset-2\"><button class=\"btn btn-primary\" type=\"submit\">Modify</button></div>\n                 </div>\n             </form>\n         </div>\n     </div>\n </div>\n</div>\n\n<div class=\"row\">\n  <div class=\"col-md-12\">\n      <div class=\"box box-primary\">\n          <div class=\"box-header with-border\">\n            <h3 class=\"box-title\">Modify Settings</h3>\n          </div>\n          <div class=\"box-body\">\n          <form role=\"form\" action=\"/settings/update/\" method=\"POST\" >\n            <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n            <div class=\"box-body\">\n              <div class=\"form-group\">\n                <label for=\"ADMIN_EMAIL_ADDRESS\">Admin Email Address</label>\n                <p class=\"help-block\">when an activating request is sent, an e-mail will be sent to this address to remind the admin. </p>\n                <p class=\"help-block\">if this address is \"\", no email will be sent to admin.</p>\n                <input type=\"input\" class=\"form-control\" id=\"ADMIN_EMAIL_ADDRESS\" value=\"{{ settings['ADMIN_EMAIL_ADDRESS'] }}\" name=\"ADMIN_EMAIL_ADDRESS\">\n              </div>\n              <div class=\"form-group\">\n                <label for=\"EMAIL_FROM_ADDRESS\">Email From Address</label>\n                <p class=\"help-block\">the e-mail address to send activating e-mail to user</p>\n                <p class=\"help-block\">if this address is \"\", no email will be sent out.</p>\n                <input type=\"input\" class=\"form-control\" id=\"EMAIL_FROM_ADDRESS\" name=\"EMAIL_FROM_ADDRESS\" value=\"{{ settings['EMAIL_FROM_ADDRESS'] }}\">\n              </div>\n              <div class=\"form-group\">\n                <label for=\"OPEN_REGISTRY\">OPEN_REGISTRY</label>\n                <p class=\"help-block\">whether allow user to register a new account</p>\n                <p class=\"help-block\">if the value is True, it will allow.</p>\n                <input type=\"input\" class=\"form-control\" id=\"OPEN_REGISTRY\" name=\"OPEN_REGISTRY\" value=\"{{ settings['OPEN_REGISTRY'] }}\">\n              </div>\n              <div class=\"form-group\">\n                <label for=\"APPROVAL_RBT\">APPROVAL_RBT</label>\n                <p class=\"help-block\">whether to start the approval robot that will approve beans applications from users automatically</p>\n                <p class=\"help-block\">if the value is True, it will allow.</p>\n                <input type=\"input\" class=\"form-control\" id=\"APPROVAL_RBT\" name=\"APPROVAL_RBT\"  value=\"{{ settings['APPROVAL_RBT'] }}\">\n              </div>\n            </div>\n            <div class=\"box-footer\">\n              <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n            </div>\n          </form>\n        </div>\n      </div>\n    </div>\n</div>\n\n {% for field in [\"docklet\"] %}\n <div class=\"row\">\n <div class=\"col-md-12\">\n <div class=\"box box-info collapsed-box\">\n      <div class=\"box-header with-border\">\n            {% if field == \"docklet\" %}\n             <h3 class=\"box-title\">Docklet Config</h3>\n             {% else %}\n             <h3 class=\"box-title\">Container Config</h3>\n             {% endif %}\n             <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-plus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i>\n                 </button>\n             </div>\n      </div>\n      <div class=\"box-body\" style=\"display:none\">\n\n        <div class=\"table table-responsive\">\n          <table id=\"myGroupTable\" class=\"table table-striped table-bordered\">\n           <thead>\n               <tr>\n                   <th>Parameter</th>\n                   <th>Value</th>\n                   <th>History (Click to Reuse)</th>\n                   <th>Default</th>\n                   <th>Command</th>\n               </tr>\n           </thead>\n           <tbody>\n              {% for editable in [1,0] %}\n               {% for parm in parms[field] %}\n               {% if parm[\"editable\"] == editable %}\n               <tr>\n\n                    {% if parm[\"parm\"]|length > 20%}\n                    <th title={{parm[\"parm\"]}}>{{ parm[\"parm\"]|truncate(20) }}</th>\n                    {% else %}\n                    <th>{{ parm[\"parm\"] }}</th>\n                    {% endif %}\n\n                    {% if parm[\"val\"] == \"novalidvaluea\" %}\n                    <th class=\"text-muted\">No Valid Value</th>\n                    {% elif parm[\"val\"]|length > 20 %}\n                    <th title=\"{{parm[\"val\"]}}\">{{ parm[\"val\"]|truncate(20) }}</th>\n                    {% else %}\n                    <th>{{ parm[\"val\"] }}</th>\n                    {% endif %}\n\n                    <th>\n                    {% for history in parm[\"history\"] %}\n                      {% if history|length > 20 %}\n                      <a class=\"btn btn-xs btn-default\" data-toggle=\"modal\" data-target=\"#UseHistoryModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}_{{ loop.indexo }}\" title=\"{{history}}\">{{ history|truncate(20) }}</a>\n                      {% else %}\n                      <a class=\"btn btn-xs btn-default\" data-toggle=\"modal\" data-target=\"#UseHistoryModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}_{{ loop.indexo }}\">{{ history }}</a>\n                      {% endif %}\n\n                        <div class=\"modal inmodal\" id=\"UseHistoryModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}_{{ loop.indexo }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                        <div class=\"modal-dialog\">\n                        <div class=\"modal-content animated fadeIn\">\n                                {% if parm[\"editable\"] == 0 %}\n                                <div class=\"modal-header\">\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4>The parameter <strong> {{ parm[\"parm\"] }} </strong> is non-editable!</h4>\n                                </div>\n                                <div class=\"modal-footer\">\n                                    <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Well</button>\n                                </div>\n                                {% else %}\n                                <div class=\"modal-header\">\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4>Sure to set <strong> {{ parm[\"parm\"] }} </strong>to <strong>{{ history }} </strong>?</h4>\n                                </div>\n                                <form action=\"/system/modify/\" method=\"POST\">\n                                  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                    <div style=\"display:none\">\n                                      <input type=\"text\" placeholder=\"\" class=\"\" name=\"field\" value={{field}} />\n                                      </div>\n                                    <div class=\"modal-body\" style=\"display:none\">\n                                      <div class=\"form-group\">\n                                          <label>Parameter</label>\n                                          <input type=\"text\" placeholder=\"Enter Parameter\" class=\"form-control\" name=\"parm\" value=\"{{ parm['parm'] }}\" readonly=\"true\" />\n                                      </div>\n                                      <div class=\"form-group\">\n                                          <label>Value</label>\n                                          <input type=\"text\" placeholder=\"Enter Value\" class=\"form-control\" name=\"val\" value=\"{{ history }}\" />\n                                      </div>\n                                    </div>\n                                    <div class=\"modal-footer\">\n                                        <button type=\"submit\" class=\"btn btn-primary\">Yes</button>\n                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">No</button>\n                                    </div>\n                                </form>\n                                {% endif %}\n                        </div>\n                        </div>\n                    </div>\n                    {% endfor %}\n                    </th>\n\n                    {% if parm[\"default\"]|length > 20 %}\n                    <th><a class=\"btn btn-xs btn-default\" data-toggle=\"modal\" data-target=\"#UseDefaultModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\" title={{ parm[\"default\"] }}> {{ parm[\"default\"]|truncate(20) }}</a></th>\n                    {% else %}\n                    <th><a class=\"btn btn-xs btn-default\" data-toggle=\"modal\" data-target=\"#UseDefaultModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\"> {{ parm[\"default\"] }}</a></th>\n                    {% endif %}\n                    <th>\n                      <a class=\"btn btn-xs btn-success\" data-toggle=\"modal\" data-target=\"#ViewParmModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\">Details</a>&nbsp;\n                    </th>\n                    <div class=\"modal inmodal\" id=\"ViewParmModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\" class=\"container\">\n                        <div class=\"modal-dialog\">\n                        <div class=\"modal-content animated fadeIn\">\n                                <div class=\"modal-header\">\n                                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                    <h4>Details of {{parm['parm']}}</h4>\n                                </div>\n                                <div class=\"modal-body\">\n                                  <pre>{{ parm['details'] }}</pre>\n                                </div>\n                                <div class=\"modal-footer\">                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                </div>\n                        </div>\n                        </div>\n                    </div>\n                    <div class=\"modal inmodal\" id=\"ModifyParmModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                        <div class=\"modal-dialog\">\n                        <div class=\"modal-content animated fadeIn\">\n                                <div class=\"modal-header\">\n                                    <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4 class=\"modal-title\">Modify Parameter</h4>\n                                    <small class=\"font-bold\">Modify a parameter in Docklet</small>\n                                </div>\n                                <form action=\"/system/modify/\" method=\"POST\" >\n                                  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                  <div style=\"display:none\">\n                                      <input type=\"text\" placeholder=\"\" class=\"\" name=\"field\" value={{field}} />\n                                      </div>\n                                    <div class=\"modal-body\">\n                                      <div class=\"form-group\">\n                                          <label>Parameter</label>\n                                          <input type=\"text\" placeholder=\"Enter Parameter\" class=\"form-control\" name=\"parm\" value=\"{{ parm['parm'] }}\" readonly=\"true\" />\n                                      </div>\n                                      <div class=\"form-group\">\n                                          <label>Value</label>\n                                          {% if parm['val'] == \"novalidvaluea\" %}\n                                          <input type=\"text\" placeholder=\"Enter Value\" class=\"form-control\" name=\"val\" value=\"\" />\n                                          {% else %}\n                                          <input type=\"text\" placeholder=\"Enter Value\" class=\"form-control\" name=\"val\" value=\"{{ parm['val'] }}\" />\n                                          {% endif %}\n                                      </div>\n                                    </div>\n                                    <div class=\"modal-footer\">\n                                        <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                    </div>\n                                </form>\n                        </div>\n                        </div>\n                    </div>\n                    <div class=\"modal inmodal\" id=\"UseDefaultModal_{{field}}_{{ parm[\"parm\"]|replace(\".\",\"\") }}\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                        <div class=\"modal-dialog\">\n                        <div class=\"modal-content animated fadeIn\">\n                                {% if parm[\"editable\"] == 0 %}\n                                <div class=\"modal-header\">\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4>The parameter <strong> {{ parm[\"parm\"] }} </strong> is non-editable!</h4>\n                                </div>\n                                <div class=\"modal-footer\">\n                                    <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Well</button>\n                                </div>\n                                {% else %}\n                                <div class=\"modal-header\">\n                                    <i class=\"fa fa-laptop modal-icon\"></i>\n                                    <h4>Sure to set <strong> {{ parm[\"parm\"] }} </strong> to <strong> {{ parm[\"default\"] }} </strong> ?</h4>\n                                </div>\n                                <form action=\"/system/modify/\" method=\"POST\">\n                                  <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                    <div style=\"display:none\">\n                                      <input type=\"text\" placeholder=\"\" class=\"\" name=\"field\" value={{field}} />\n                                      </div>\n                                    <div class=\"modal-body\" style=\"display:none\">\n                                      <div class=\"form-group\">\n                                          <label>Parameter</label>\n                                          <input type=\"text\" placeholder=\"Enter Parameter\" class=\"form-control\" name=\"parm\" value=\"{{ parm['parm'] }}\" readonly=\"true\" />\n                                      </div>\n                                      <div class=\"form-group\">\n                                          <label>Value</label>\n                                          <input type=\"text\" placeholder=\"Enter Value\" class=\"form-control\" name=\"val\" value=\"{{ parm[\"default\"] }}\" />\n                                      </div>\n                                    </div>\n                                    <div class=\"modal-footer\">\n                                        <button type=\"submit\" class=\"btn btn-primary\">Yes</button>\n                                        <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">No</button>\n                                    </div>\n                                </form>\n                                {% endif %}\n                        </div>\n                        </div>\n                    </div>\n               </tr>\n               {% endif %}\n               {% endfor %}\n               {% endfor %}\n           </tbody>\n         </table>\n         </div>\n      </div>\n    </div>\n </div>\n</div>\n{% endfor %}\n\n <div class=\"row\">\n <div class=\"col-md-12\">\n <div class=\"box box-info collapsed-box\">\n      <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">Container Config</h3>\n             <div class=\"box-tools pull-right\">\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-plus\"></i>\n                 </button>\n                 <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i>\n                 </button>\n             </div>\n      </div>\n      <div class=\"box-body\" style=\"display:none\">\n      <pre> {{ parms[\"container\"] }} </pre>\n      </div>\n      </div>\n      </div>\n      </div>\n\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables-tabletools/2.1.5/js/TableTools.min.js\"></script>\n\n<script type=\"text/javascript\">\n    $(document).ready(function() {\n      $('#myGroupTable').DataTable();\n    })\n    function sendAddGroup(){\n        document.getElementById(\"addGroupForm\").submit();\n    }\n    function sendAddQuota(){\n        document.getElementById(\"addQuotaForm\").submit();\n    }\n    function sendChDefault(){\n        document.getElementById(\"chDefaultForm\").submit();\n    }\n    function setFormGroup(arg){\n      $.post(\"/group/query/\",\n      {\n        name: arg\n      },\n      function(data,status){\n        var result = eval(\"(\"+data+\")\").data;\n        $(\"#mGroupname\").val(result.name);\n        $(\"#mCpu\").val(result.cpu);\n        $(\"#mMemory\").val(result.memory);\n        $(\"#mImage\").val(result.imageQuantity);\n        $(\"#mLifecycle\").val(result.lifeCycle);\n      });\n    }\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/templates/user/activate.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"utf-8\">\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n  <title>Docklet | Login</title>\n  <!-- Tell the browser to be responsive to screen width -->\n  <meta content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" name=\"viewport\">\n  <!-- Bootstrap 3.3.5 -->\n  <link href=\"//cdn.bootcss.com/bootstrap/3.3.5/css/bootstrap.min.css\" rel=\"stylesheet\">\n  <!-- Font Awesome -->\n  <link href=\"//cdn.bootcss.com/font-awesome/4.3.0/css/font-awesome.min.css\" rel=\"stylesheet\">\n  <!-- Ionicons -->\n  <link href=\"//cdn.bootcss.com/ionicons/2.0.1/css/ionicons.min.css\" rel=\"stylesheet\">\n  <!-- Theme style -->\n  <link rel=\"stylesheet\" href=\"/static/dist/css/AdminLTE.min.css\">\n\n  <link rel=\"shortcut icon\" href=\"/static/img/favicon.ico\">\n\n\n</head>\n<body class=\"hold-transition login-page\">\n<div class=\"login-box\">\n  <div class=\"login-logo\">\n    <img src=\"/static/img/logo.png\" class=\"logo-name\" height=\"50%\" width=\"50%\">\n    <!--a href=\"/\"><b>Docklet</b></a-->\n  </div>\n  <!-- /.login-logo -->\n  <div class=\"login-box-body\">\n    <p class=\"login-box-msg\">An easy and quick way to launch your DISTRIBUTED applications!</p>\n            <form class=\"m-t\" role=\"form\" action=\"\" id=\"activateForm\" method=\"POST\">\n                <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                <div class=\"form-group\">\n                    <input type=\"email\" class=\"form-control\" placeholder=\"E-mail\" required=\"\" name=\"email\" value=\"{{ info['e_mail'] }}\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"Student number or Staff number\" required=\"\" name=\"studentnumber\" value=\"{{ info['student_number'] }}\" >\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"Department e.g. SEI, EECS\" required=\"\" name=\"department\" value=\"{{ info['department'] }}\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"True Name e.g. Zhang San\" required=\"\" name=\"truename\" value=\"{{ info['truename'] }}\">\n                </div>\n                <div class=\"form-group\">\n                    <input type=\"text\" class=\"form-control\" placeholder=\"Telephone Number\" required=\"\" name=\"tel\" value=\"{{ info['tel'] }}\">\n                </div>\n                <div class=\"form-group\">\n                    <textarea  class=\"form-control\"  name=\"description\" form=\"activateForm\" id=\"mDescription\">\n{{ info['description'] }}\n                    </textarea>\n                </div>\n                <input type=\"hidden\" name=\"activate\" value=\"true\">\n                <div class=\"row\">\n\n                  <div class=\"col-xs-12\">\n                    <button type=\"submit\" class=\"btn btn-primary btn-block btn-flat\">Activate</button>\n                  </div>\n                </div>\n                <!--p class=\"text-muted text-center\"><small>Do not have an account?</small></p-->\n                <!--a class=\"btn btn-sm btn-white btn-block\" href=\"register.html\">Create an account</a-->\n            </form>\n          </div>\n          <!-- /.login-box-body -->\n        </div>\n        <!-- /.login-box -->\n\n        <!-- jQuery 2.2.1 -->\n        <script src=\"//cdn.bootcss.com/jquery/2.2.1/jquery.min.js\"></script>\n        <!-- Bootstrap 3.3.5 -->\n        <script src=\"//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js\"></script>\n\n        </body>\n        </html>\n"
  },
  {
    "path": "web/templates/user/info.html",
    "content": "{% extends 'base_AdminLTE.html' %}\n\n{% block title %}Docklet | Information Modify{% endblock %}\n\n{% block css_src %}\n<link href=\"//cdn.bootcss.com/x-editable/1.5.1/bootstrap3-editable/css/bootstrap-editable.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n{% endblock %}\n\n{% block panel_title %}Detail for User Infomation{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\">Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>User Info</strong>\n  </li>\n</ol>\n{% endblock %}\n\n\n{% block content %}\n<div class=\"row\">\n    <div class=\"col-md-5\">\n      <div class=\"box box-info\">\n           <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">User Info</h3>\n\n             <div class=\"box-tools pull-right\">\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n               </button>\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n             </div>\n           </div>\n            <div class=\"box-body\">\n                <table class=\"table table-bordered\">\n                    <tr>\n                        <td>User Name</td>\n                        <td>{{ info['username'] }}</td>\n                    </tr>\n                    <tr>\n                        <td>Nickname</td>\n                        <td><a href=\"#\" id=\"nickname\" data-type=\"text\" data-pk=\"1\" data-url=\"/user/info/\" data-title=\"Enter nickname\">{{ info['nickname'] }}</a></td>\n                    </tr>\n                    <tr>\n                        <td>Description</td>\n                        <td><a href=\"#\" id=\"description\" data-type=\"text\" data-pk=\"1\" data-url=\"/user/info/\" data-title=\"Enter description\">{{ info['description'] }}</a></td>\n                    </tr>\n                    <tr>\n                        <td>Truename</td>\n                        <td>{{ info['truename'] }}</a></td>\n                    </tr>\n                    <tr>\n                        <td>Status</td>\n                        <td>{{ info['status'] }}</td>\n                    </tr>\n                    <tr>\n                        <td>E-mail</td>\n                        <td><a href=\"#\" id=\"e_mail\" data-type=\"text\" data-pk=\"1\" data-url=\"/user/info/\" data-title=\"Enter e-mail\">{{ info['e_mail'] }}</a></td>\n                    </tr>\n                    <tr>\n                        <td>Department</td>\n                        <td><a href=\"#\" id=\"department\" data-type=\"text\" data-pk=\"1\" data-url=\"/user/info/\" data-title=\"Enter department\">{{ info['department'] }}</a></td>\n                    </tr>\n                    <tr>\n                        <td>ID Number</td>\n                        <td>{{ info['student_number'] }}</td>\n                    </tr>\n                    <tr>\n                        <td>Telephone</td>\n                        <td><a href=\"#\" id=\"tel\" data-type=\"text\" data-pk=\"1\" data-url=\"/user/info/\" data-title=\"Enter telephone number\">{{ info['tel'] }}</a></td>\n                    </tr>\n                    {% if info['auth_method'] == 'local' %}\n                    <tr>\n                        <td>password</td>\n                        <td>\n                          <div class=\"col-md-12\" >\n                            <button type=\"button\" class=\"btn btn-primary btn-xs btn-block\" data-toggle=\"modal\" data-target=\"#ChpasswordModal\"> Change Password</button>\n                          </div>\n                          <div class=\"modal inmodal\" id=\"ChpasswordModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                              <div class=\"modal-dialog\">\n                              <div class=\"modal-content animated fadeIn\">\n                                      <div class=\"modal-header\">\n                                          <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                          <i class=\"fa fa-laptop modal-icon\"></i>\n                                          <h4 class=\"modal-title\">Change Password</h4>\n                                          <small class=\"font-bold\">modify your password</small>\n                                      </div>\n                                      <div class=\"modal-body\">\n\n                                           <form action=\"/user/info/\" method=\"POST\" id=\"ChpasswordForm\">\n                                             <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                             <div class=\"form-group\">\n                                               <label>Old password</label>\n                                               <input type = \"password\" placeholder=\"Enter old password\" class=\"form-control\" name=\"o_password\" id=\"o_password\">\n                                             </div>\n                                             <div class=\"form-group\">\n                                               <label>New password</label>\n                                               <input type = \"password\" placeholder=\"Enter new password\" class=\"form-control\" name=\"n_password\" id = \"n_password\">\n                                             </div>\n                                             <div class=\"form-group\">\n                                               <label>Verify</label>\n                                               <input type = \"password\" placeholder=\"Enter new password again\" class=\"form-control\" name=\"v_password\" id = \"v_password\">\n                                             </div>\n                                            <p style=\"color:red\" id=\"notCorrectFlag\"></p>\n                                           </form>\n\n                                      </div>\n                                      <div class=\"modal-footer\">\n                                          <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                                          <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendChpassword();\">Submit</button>\n                                      </div>\n                                  </div>\n                              </div>\n                          </div>\n                        </td>\n\n                    </tr>\n                    {% endif %}\n\n\n                  </table>\n            </div>\n        </div>\n    </div>\n    <!--\n    <div class=\"col-lg-7\">\n        <div class=\"ibox float-e-margins\">\n\t           <div class=\"ibox-title back-change\">\n                <h5>Avatar <small>Upload your avatar</small></h5>\n                <div class=\"ibox-tools\">\n\t                   <a class=\"collapse-link\">\n\t                      <i class=\"fa fa-chevron-up\"></i>\n\t                   </a>\n\t                   <a class=\"dropdown-toggle\" data-toggle=\"dropdown\" href=\"#\">\n\t                      <i class=\"fa fa-wrench\"></i>\n\t                   </a>\n\t                        <a class=\"close-link\">\n\t                           <i class=\"fa fa-times\"></i>\n\t                        </a>\n\t              </div>\n\t             </div>\n          \t<div class=\"ibox-content\">\n\n          \t<div class=\"row\">\n          \t<div class=\"col-md-6\">\n          \t<div class=\"image-crop\">\n          \t<img src=\"/static/img/profile.png\">\n          \t</div>\n          \t</div>\n          \t<div class=\"col-md-6\">\n          \t<h4>Preview image</h4>\n          \t<div class=\"img-preview img-preview-sm\"></div>\n          \t<h4>Comon method</h4>\n          \t<p>\n          \tYou can upload new image to crop container and easy upload as your avatar\n          \t</p>\n          \t<div class=\"btn-group\">\n          \t<label title=\"Upload image file\" for=\"inputImage\" class=\"btn btn-primary\">\n          \t<input type=\"file\" accept=\"image/*\" name=\"file\" id=\"inputImage\" class=\"hide\">\n          \tUpload new image\n          \t</label>\n          \t<label title=\"setavatar\" id=\"download\" class=\"btn btn-primary\">Set as avatar</label>\n          \t</div>\n          \t<h4>Other method</h4>\n          \t<p>\n              Some methods to process the image\n          \t</p>\n          \t<div class=\"btn-group\">\n          \t<button class=\"btn btn-white\" id=\"zoomIn\" type=\"button\">Zoom In</button>\n          \t<button class=\"btn btn-white\" id=\"zoomOut\" type=\"button\">Zoom Out</button>\n          \t<button class=\"btn btn-white\" id=\"rotateLeft\" type=\"button\">Rotate Left</button>\n          \t<button class=\"btn btn-white\" id=\"rotateRight\" type=\"button\">Rotate Right</button>\n          \t</div>\n          \t</div>\n          \t</div>\n          \t</div>\n          \t</div>\n          \t</div>\n          -->\n          \t</div>\n\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/x-editable/1.5.1/bootstrap3-editable/js/bootstrap-editable.min.js\"></script>\n<script type=\"text/javascript\">\n$(document).ready(function(){\n    $.fn.editable.defaults.mode = 'popup';\n    $('#nickname').editable();\n    $('#description').editable();\n    $('#department').editable();\n    $('#e_mail').editable();\n    $('#tel').editable();\n    //var $image = $(\".image-crop > img\")\n    //$($image).cropper({\n    //    aspectRatio: 1,\n    //    preview: \".img-preview\",\n    //    done: function(data) {\n            // Output the result data for cropping image.\n    //    }\n    //});\n    /*\n    var $inputImage = $(\"#inputImage\");\n    if (window.FileReader) {\n        $inputImage.change(function() {\n            var fileReader = new FileReader(),\n                    files = this.files,\n                    file;\n\n            if (!files.length) {\n                return;\n            }\n\n            file = files[0];\n\n            if (/^image\\/\\w+$/.test(file.type)) {\n                fileReader.readAsDataURL(file);\n                fileReader.onload = function () {\n                    $inputImage.val(\"\");\n                    $image.cropper(\"reset\", true).cropper(\"replace\", this.result);\n                };\n            } else {\n                showMessage(\"Please choose an image file.\");\n            }\n        });\n    } else {\n        $inputImage.addClass(\"hide\");\n    }\n\n    $(\"#download\").click(function() {\n        window.open($image.cropper(\"getDataURL\"));\n    });\n\n    $(\"#zoomIn\").click(function() {\n        $image.cropper(\"zoom\", 0.1);\n    });\n\n    $(\"#zoomOut\").click(function() {\n        $image.cropper(\"zoom\", -0.1);\n    });\n\n    $(\"#rotateLeft\").click(function() {\n        $image.cropper(\"rotate\", 45);\n    });\n\n    $(\"#rotateRight\").click(function() {\n        $image.cropper(\"rotate\", -45);\n    });\n    */\n\n  });\n  function sendChpassword(){\n      if ($(\"#n_password\").val() == $(\"#v_password\").val())\n      {\n        $.post(\"/user/info/\",\n        {\n          name: 'password',\n          value: $(\"#n_password\").val(),\n          old_value : $(\"#o_password\").val()\n        },\n        function(data,status){\n          var result = eval(\"(\"+data+\")\");\n          if (result.success == 'true')\n          {\n            location.reload();\n          }\n          else\n          {\n            $(\"#notCorrectFlag\").html(\"Cannot modify your password\");\n          }\n        });\n      }\n      else\n      {\n        $(\"#notCorrectFlag\").html(\"Two passwords are not identical\");\n      }\n    }\n\n\n\n  </script>\n\n\n{% endblock %}\n"
  },
  {
    "path": "web/templates/user/mailservererror.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\r\n\r\n\r\n{% block title %}Docklet | Error{% endblock %}\r\n\r\n{% block panel_title %}500 Error Page{% endblock %}\r\n\r\n{% block panel_list %}\r\n<ol class=\"breadcrumb\">\r\n  <li>\r\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\r\n  </li>\r\n</ol>\r\n{% endblock %}\r\n\r\n{% block content %}\r\n\r\n<div class=\"error-page\">\r\n    <h2 class=\"headline text-red\">500</h2>\r\n\r\n        <div class=\"error-content\">\r\n          <h3><br/><i class=\"fa fa-warning text-red\"></i> Internal Server Error</h3>\r\n\r\n          <p>\r\n          Please examine your mail server config(now exim4).You can go back to\r\n            <a href=\"/dashboard/\">dashboard</a> or <a href=\"/logout\">log out</a>\r\n          </p>\r\n        </div>\r\n</div>\r\n{% endblock %}\r\n"
  },
  {
    "path": "web/templates/user_list.html",
    "content": "{% extends \"base_AdminLTE.html\"%}\n{% block title %}Docklet | UserList{% endblock %}\n\n{% block panel_title %}UserList{% endblock %}\n\n{% block panel_list %}\n<ol class=\"breadcrumb\">\n  <li>\n      <a href=\"/dashboard/\"><i class=\"fa fa-dashboard\"></i>Home</a>\n  </li>\n  <li class=\"active\">\n      <strong>UserList</strong>\n  </li>\n</ol>\n{% endblock %}\n\n{% block css_src %}\n\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/dataTables.bootstrap.min.css\" rel=\"stylesheet\">\n<link href=\"//cdn.bootcss.com/datatables/1.10.11/css/jquery.dataTables_themeroller.css\" rel=\"stylesheet\">\n<link href=\"/static/dist/css/modalconfig.css\" rel=\"stylesheet\">\n\n\n{% endblock %}\n\n\n{% block content %}\n<div class=\"row\">\n  <div class=\"col-md-12\">\n      <div class=\"box box-info\">\n           <div class=\"box-header with-border\">\n             <h3 class=\"box-title\">User List</h3>\n\n             <div class=\"box-tools pull-right\">\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n               </button>\n               <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n             </div>\n           </div>\n           <div class=\"box-body\">\n                <button type=\"button\" class=\"btn btn-primary btn-sm\" data-toggle=\"modal\" data-target=\"#AddUserModal\"><i class=\"fa fa-plus\"></i> Add User</button>\n\t<div class=\"modal inmodal\" id=\"AddUserModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n             <div class=\"modal-dialog\">\n             <div class=\"modal-content animated fadeIn\">\n                     <div class=\"modal-header\">\n                         <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                         <i class=\"fa fa-laptop modal-icon\"></i>\n                         <h4 class=\"modal-title\">Add User</h4>\n                         <small class=\"font-bold\">Add a user in Docklet</small>\n                     </div>\n                     <div class=\"modal-body\">\n                       <form action=\"/user/add/\" method=\"POST\" id=\"addUserForm\">\n                         <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                          <div class=\"form-group\">\n                            <label>User Name</label>\n                            <input type = \"text\" placeholder=\"Enter Username\" class=\"form-control\" name=\"username\">\n                          </div>\n                          <div class=\"form-group\">\n                            <label>PASSWORD</label>\n                            <input type = \"password\" placeholder=\"Enter Password\" class=\"form-control\" name=\"password\">\n                          </div>\n                          <div class=\"form-group\">\n                            <label>E-mail</label>\n                            <input type=\"email\" placeholder=\"Enter E-mail Address\" class=\"form-control\" name=\"e_mail\">\n                          </div>\n                        </form>\n                     </div>\n                     <div class=\"modal-footer\">\n                         <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                         <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendAddUser();\">Submit</button>\n                     </div>\n             </div>\n             </div>\n           </div>\n              <div class=\"table table-responsive\">\n               <table id=\"myDataTable\" class=\"table table-striped table-bordered\">\n                 <thead>\n                     <tr>\n                         <th>ID</th>\n                         <th>User</th>\n                         <th>Name</th>\n                         <th>E_mail</th>\n                         <th>Tel</th>\n                         <th>RegisterDate</th>\n                         <th>Status</th>\n                         <th>Group</th>\n                         <th>Beans</th>\n                         <th>Command</th>\n                     </tr>\n                 </thead>\n                 <tbody>\n                 </tbody>\n                 <div class=\"modal inmodal\" id=\"ModifyUserModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                     <div class=\"modal-dialog\">\n                     <div class=\"modal-content animated fadeIn\">\n                             <div class=\"modal-header\">\n                                 <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                 <i class=\"fa fa-laptop modal-icon\"></i>\n                                 <h4 class=\"modal-title\">Modify User</h4>\n                                 <small class=\"font-bold\">Modify a user in Docklet</small>\n                             </div>\n                             <div class=\"modal-body\">\n                               <form action=\"/user/modify/\" method=\"POST\" id=\"modifyUserForm\">\n                                 <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                  <div class=\"form-group\">\n                                    <label>User Name</label>\n                                    <input type = \"text\" placeholder=\"Enter Username\" class=\"form-control\" name=\"username\" id=\"mUsername\" readonly=\"readonly\">\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>Status</label>\n                                    <select class=\"form-control\" name=\"status\" id=\"mStatus\">\n                                          <option>normal</option>\n                                          <option>applying</option>\n                                          <option>init</option>\n                                          <option>locked</option>\n                                    </select>\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>True Name</label>\n                                    <input type = \"text\" placeholder=\"Enter Truename\" class=\"form-control\" name=\"truename\" id=\"mTruename\">\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>E-mail</label>\n                                    <input type=\"email\" placeholder=\"Enter E-mail Address\" class=\"form-control\" name=\"e_mail\" id=\"mE_mail\">\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>Department</label>\n                                    <input type = \"text\" placeholder=\"Enter Department\" class=\"form-control\" name=\"department\" id=\"mDepartment\">\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>Student Number</label>\n                                    <input type = \"text\" placeholder=\"Enter Student Number\" class=\"form-control\" name=\"student_number\" id=\"mStudentNumber\">\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>Telephone Number</label>\n                                    <input type = \"text\" placeholder=\"Enter Telephone Number\" class=\"form-control\" name=\"tel\" id=\"mTel\">\n                                  </div>\n\n                                  <!--div class=\"form-group\">\n                                    <label>Password</label>\n                                    <input type=\"password\" placeholder=\"Enter Password\" class=\"form-control\" name=\"password\" id=\"mPassword\">\n                                  </div-->\n\n                                  <div class=\"form-group\">\n                                    <label>User Group</label>\n                                    <select class=\"form-control\" name=\"group\" id=\"mUserGroup\">\n                                        {% for group in groups %}\n                                          <option>{{ group }}</option>\n                                        {% endfor %}\n                                    </select>\n                                  </div>\n                                  <div class=\"form-group\">\n                                    <label>Auth Method</label>\n                                    <select class=\"form-control\" name=\"auth_method\" id=\"mAuthMethod\">\n                                          <option>local</option>\n                                          <option>pam</option>\n                                          <option>iaaa</option>\n                                    </select>\n                                  </div>\n\n                                  <div class=\"form-group\">\n                                    <label>Description</label>\n                                    <textarea class=\"form-control\" name=\"description\" id=\"mDescription\" readonly=\"readonly\">\n\n                                    </textarea>\n                                  </div>\n                                </form>\n                       </div>\n                       <div class=\"modal-footer\">\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                           <a class=\"btn btn-danger\" data-toggle=\"modal\" data-target=\"#ChpasswordModal\" onClick=\"\" id=\"ChpasswordButton\">Change Password</a>\n                           <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendModifyUser();\">Submit</button>\n                       </div>\n                     </div>\n                     </div>\n                 </div>\n                 <div class=\"modal inmodal\" id=\"ChpasswordModal\" tabindex=\"-1\" role=\"dialog\" aria-hidden=\"true\">\n                     <div class=\"modal-dialog\">\n                     <div class=\"modal-content animated fadeIn\">\n                             <div class=\"modal-header\">\n                                 <button type=\"button\" class=\"close\" data-dismiss=\"modal\"><span aria-hidden=\"true\">&times;</span><span class=\"sr-only\">Close</span></button>\n                                 <i class=\"fa fa-laptop modal-icon\"></i>\n                                 <h4 class=\"modal-title\">Changing Password</h4>\n                             </div>\n                             <div class=\"modal-body\">\n                               <form action=\"/user/change/\" method=\"POST\" id=\"chpasswordForm\">\n                                 <input type=\"hidden\" name=\"csrf_token\" value=\"{{ csrf_token() }}\">\n                                  <div class=\"form-group\">\n                                    <label>User Name</label>\n                                    <input type = \"text\" placeholder=\"Enter Username\" class=\"form-control\" name=\"username\" id=\"mpUsername\" readonly=\"readonly\">\n                                  </div>\n\n                                  <div class=\"form-group\">\n                                    <label>Password</label>\n                                    <input type=\"password\" placeholder=\"Enter Password\" class=\"form-control\" name=\"password\" id=\"mpPassword\">\n                                  </div>\n                               </form>\n                               <div class=\"form-group\">\n                                 <label>Retype Password</label>\n                                 <input type=\"password\" placeholder=\"Enter Password\" class=\"form-control\" id=\"mpPassword2\">\n                               </div>\n                               <p style=\"color:red\" id=\"notCorrectFlag\"></p>\n\n                       </div>\n                       <div class=\"modal-footer\">\n                           <button type=\"button\" class=\"btn btn-white\" data-dismiss=\"modal\">Close</button>\n                           <button type=\"button\" class=\"btn btn-primary\" onClick=\"javascript:sendChpassword();\">Submit</button>\n                       </div>\n                     </div>\n                     </div>\n                 </div>\n\n         </table>\n       </div>\n\n     </div>\n  </div>\n</div>\n</div>\n<div class=\"row\">\n<div class=\"col-md-12\">\n<div class=\"box box-info\">\n    <div class=\"box-header with-border\">\n       <h3 class=\"box-title\">Processing Beans Applications</h3>\n       <div class=\"box-tools pull-right\">\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"collapse\"><i class=\"fa fa-minus\"></i>\n         </button>\n         <button type=\"button\" class=\"btn btn-box-tool\" data-widget=\"remove\"><i class=\"fa fa-times\"></i></button>\n       </div>\n     </div>\n       <div class=\"box-body\">\n                   <div class=\"table table-responsive\">\n                     <table class=\"table table-striped table-bordered table-hover table-image\" >\n                        <thead>\n                    \t<tr>\n                        <th>Application ID</th>\n                        <th>Username</th>\n                        <th>Number</th>\n                        <th>Submission Time</th>\n                        <th>Reason</th>\n                        <th>Command</th>\n                    \t</tr>\n                    \t</thead>\n                    <tbody>\n                      {% for application in applications %}\n                      <tr>\n                      <td>{{ application.id }}</td>\n                      <td>{{ application.username }}</td>\n                      <td>{{ application.number }} beans</td>\n                      <td>{{ application.time }}</td>\n                      <td>{{ application.reason }}</td>\n                      <td><a class=\"btn btn-xs btn-info\" href=\"/beans/admin/{{ application.username }}/{{ application.id }}/agree/\">Agree</a>&nbsp;&nbsp;&nbsp;&nbsp;\n                          <a class=\"btn btn-xs btn-danger\" href=\"/beans/admin/{{ application.username }}/{{ application.id }}/reject/\">Reject</a></td>\n                      </tr>\n                      {% endfor %}\n                    </tbody>\n\t\t  </table>\n                </div>\n\t  </div>\n        </div>\n</div>\n</div>\n{% endblock %}\n\n{% block script_src %}\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/jquery.dataTables.min.js\"></script>\n<script src=\"//cdn.bootcss.com/datatables/1.10.11/js/dataTables.bootstrap.min.js\"></script>\n\n\n<script type=\"text/javascript\">\n    $(document).ready(function() {\n      var oTable = $('#myDataTable').dataTable({\n        \"ajax\": {\n              \"url\": \"/user/list/\",\n              \"type\": \"POST\"\n          },\n      //\"scrollX\": true,\n      \"columnDefs\": [\n            {\n                \"render\": function ( data, type, row ) {\n                    str='<a class=\"btn btn-info btn-xs\" data-toggle=\"modal\" data-target=\"#ModifyUserModal\" onClick=\"javascript:setFormUser(' + row[0] + ');\">' + 'Edit' + '</a>';\n                    if (row[6]=='applying')\n                    {\n                      str=str + '<a class=\"btn btn-danger btn-xs\" onClick=\"javascript:setActivateUser(' + row[0] + ');\">' + 'Activate' + '</a>';\n                    }\n                    str += '&nbsp;<a class=\"btn btn-warning btn-xs\" onClick=\"javascript:releaseLock(\\'' + row[1] + '\\');\">' + 'Release Lock' + '</a>';\n                    return str;\n                },\n                \"targets\": 9\n            },\n          ]\n\n      });\n      var gTable = $('#myGroupTable').dataTable({\n        \"ajax\": {\n              \"url\": \"/group/detail/\",\n              \"type\": \"POST\"\n          },\n      //\"scrollX\": true,\n      \"columnDefs\": [\n            {\n                \"render\": function ( data, type, row ) {\n                    return '<a class=\"btn btn-info btn-sm\" data-toggle=\"modal\" data-target=\"#ModifyGroupModal\" onClick=\"javascript:setFormGroup(' + row[0] + ');\">' + 'Edit' + '</a>';\n                },\n                \"targets\": 6\n            },\n          ]\n\n      });\n    });\n    function sendAddUser(){\n\t\t    document.getElementById(\"addUserForm\").submit();\n    }\n    function sendAddGroup(){\n        document.getElementById(\"addGroupForm\").submit();\n    }\n    function sendModifyUser(){\n        document.getElementById(\"modifyUserForm\").submit();\n    }\n    function sendChpassword(){\n        if ($(\"#mpPassword\").val() == $(\"#mpPassword2\").val())\n        {\n          document.getElementById(\"chpasswordForm\").submit();\n        }\n        else\n        {\n          $(\"#notCorrectFlag\").html(\"Two passwords are not identical\");\n        }\n    }\n    function sendModifyGroup(){\n        document.getElementById(\"modifyGroupForm\").submit();\n    }\n    function setFormUser(arg){\n      $.post(\"/user/query/\",\n      {\n        ID: arg,\n      },\n      function(data,status){\n        var result = eval(\"(\"+data+\")\").data;\n        $(\"#mUsername\").val(result.username);\n        $(\"#mTruename\").val(result.truename);\n        $(\"#mE_mail\").val(result.e_mail);\n        $(\"#mDepartment\").val(result.department);\n        $(\"#mStudentNumber\").val(result.student_number);\n        $(\"#mTel\").val(result.tel);\n        $(\"#mStatus\").val(result.status);\n        $(\"#mUserGroup\").val(result.group);\n        $(\"#mAuthMethod\").val(result.auth_method);\n        $(\"#mDescription\").val(result.description);\n        $(\"#ChpasswordButton\").attr(\"onClick\", \"javascript:setFormChpassword(\\\"\" + result.username + \"\\\");\");\n      });\n    }\n    function releaseLock(uname){\n      $.post(\"/user/lock/release/\"+uname+\"/\",{},\n      function(data,status){\n        alert(data);\n      });\n    }\n    function setFormChpassword(arg){\n      $(\"#mpUsername\").val(arg);\n    }\n    function setActivateUser(arg){\n      $.post(\"/user/change/\",\n      {\n        ID: arg,\n        Instruction: \"Activate\",\n      },\n      function(data,status){\n        location.reload();\n      });\n    }\n    $(document).ready(function() {\n      $(\".table-image\").DataTable();\n    });\n</script>\n{% endblock %}\n"
  },
  {
    "path": "web/web.py",
    "content": "#!/usr/bin/python3\nimport json\nimport os\nimport getopt\n\nimport sys, inspect\n\n\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))\nsrc_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,\"..\", \"src\")))\nif src_folder not in sys.path:\n    sys.path.insert(0, src_folder)\n\n# must first init loadenv\nfrom utils import tools, env\nconfig = env.getenv(\"CONFIG\")\ntools.loadenv(config)\n\nfrom webViews.log import initlogging\ninitlogging(\"docklet-web\")\nfrom webViews.log import logger\n\nfrom flask import Flask, request, session, render_template, redirect, send_from_directory, make_response, url_for, abort\nfrom flask_wtf.csrf import CsrfProtect\nfrom webViews.dashboard import dashboardView\nfrom webViews.user.userlist import userlistView, useraddView, usermodifyView, userdataView, userqueryView\nfrom webViews.notification.notification import CreateNotificationView, NotificationView, QuerySelfNotificationsView, \\\n    QueryNotificationView, ModifyNotificationView, DeleteNotificationView\nfrom webViews.user.userinfo import userinfoView\nfrom webViews.user.userActivate import userActivateView\nfrom webViews.syslogs import logsView\nfrom webViews.user.grouplist import grouplistView, groupqueryView, groupdetailView, groupmodifyView\nfrom functools import wraps\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.cluster import *\nfrom webViews.admin import *\nfrom webViews.monitor import *\nfrom webViews.beansapplication import *\nfrom webViews.cloud import *\nfrom webViews.reportbug import *\nfrom webViews.authenticate.auth import login_required, administration_required,activated_required\nfrom webViews.authenticate.register import registerView\nfrom webViews.authenticate.login import loginView, logoutView\nfrom webViews.batch import *\nimport webViews.dockletrequest\nfrom webViews import cookie_tool\nimport traceback\n\nfrom werkzeug.utils import secure_filename\n\n\n\nexternal_login = env.getenv('EXTERNAL_LOGIN')\n#default config\nexternal_login_url = '/external_auth/'\nexternal_login_callback_url = '/external_auth_callback/'\nif (external_login == 'True'):\n    sys.path.insert(0, os.path.realpath(os.path.abspath(os.path.join(this_folder,\"../src\", \"plugin\"))))\n    import external_generate\n    from webViews.authenticate.login import external_loginView, external_login_callbackView\n    external_login_url = external_generate.external_login_url\n    external_login_callback_url = external_generate.external_login_callback_url\n\n\napp = Flask(__name__)\nCsrfProtect(app)\n\n\n@app.route(\"/\", methods=['GET'])\ndef home():\n    return render_template('home.html')\n\n@app.route(\"/login/\", methods=['GET', 'POST'])\ndef login():\n    loginView.open_registry = os.environ[\"OPEN_REGISTRY\"]\n    return loginView.as_view()\n\n@app.route(external_login_url, methods=['GET'])\ndef external_login_func():\n    try:\n        return external_loginView.as_view()\n    except:\n        abort(404)\n\n@app.route(external_login_callback_url, methods=['GET'])\ndef external_login_callback():\n    try:\n        return external_login_callbackView.as_view()\n    except:\n        abort(404)\n\n@app.route(\"/logout/\", methods=[\"GET\"])\n@login_required\ndef logout():\n    return logoutView.as_view()\n\n@app.route(\"/register/\", methods=['GET', 'POST'])\n#@administration_required\n#now forbidden,only used by SEI & PKU Staffs and students.\n#can be used by admin for testing\ndef register():\n    return registerView.as_view()\n\n\n\n@app.route(\"/activate/\", methods=['GET', 'POST'])\n@login_required\ndef activate():\n    return userActivateView.as_view()\n\n@app.route(\"/dashboard/\", methods=['GET'])\n@login_required\ndef dashboard():\n    return dashboardView.as_view()\n\n@app.route(\"/document/\", methods=['GET'])\ndef redirect_dochome():\n    return redirect(\"https://unias.github.io/docklet/userguide/\")\n\n@app.route(\"/config/\", methods=['GET'])\n@login_required\ndef config_view():\n    return configView.as_view()\n\n@app.route(\"/bug/report/\", methods=['POST'])\n@login_required\ndef reportBug():\n    reportBugView.bugmessage = request.form['bugmessage']\n    return reportBugView.as_view()\n\n@app.route(\"/admin_batch_list/\", methods=['GET'])\n@login_required\ndef batch_admin_job():\n    return batchAdminListView().as_view()\n\n@app.route(\"/batch_jobs/\", methods=['GET'])\n@login_required\ndef batch_job():\n    return batchJobListView().as_view()\n\n@app.route(\"/batch_job/create/\", methods=['GET'])\n@login_required\ndef create_batch_job():\n    return createBatchJobView().as_view()\n\n@app.route(\"/batch_job/<masterip>/add/\", methods=['POST'])\n@login_required\ndef add_batch_job(masterip):\n    addBatchJobView.masterip = masterip\n    addBatchJobView.job_data = request.form\n    return addBatchJobView().as_view()\n\n@app.route(\"/batch_job/<masterip>/stop/<jobid>/\", methods=['GET'])\n@login_required\ndef stop_batch_job(masterip,jobid):\n    stopBatchJobView.masterip = masterip\n    stopBatchJobView.jobid = jobid\n    return stopBatchJobView().as_view()\n\n@app.route(\"/admin_batch_job/<masterip>/stop/<jobid>/\", methods=['GET'])\n@login_required\ndef admin_stop_batch_job(masterip,jobid):\n    adminStopBatchJobView.masterip = masterip\n    adminStopBatchJobView.jobid = jobid\n    return adminStopBatchJobView().as_view()\n\n@app.route(\"/batch_job/<masterip>/info/<jobid>/\", methods=['GET'])\n@login_required\ndef info_batch_job(masterip,jobid):\n    infoBatchJobView.masterip = masterip\n    infoBatchJobView.jobid = jobid\n    return infoBatchJobView().as_view()\n\n@app.route(\"/batch_job/output/<masterip>/<jobid>/<taskid>/<vnodeid>/<issue>/\", methods=['GET'])\n@login_required\ndef output_batch_job(masterip, jobid, taskid, vnodeid, issue):\n    outputBatchJobView.masterip = masterip\n    outputBatchJobView.jobid = jobid\n    outputBatchJobView.taskid = taskid\n    outputBatchJobView.vnodeid = vnodeid\n    outputBatchJobView.issue = issue\n    return outputBatchJobView().as_view()\n\n@app.route(\"/batch/job/output/<masterip>/<jobid>/<taskid>/<vnodeid>/<issue>/\", methods=['POST'])\n@login_required\ndef output_batch_job_request(masterip, jobid, taskid, vnodeid, issue):\n    data = {\n        'jobid':jobid,\n        'taskid':taskid,\n        'vnodeid':vnodeid,\n        'issue':issue\n    }\n    result = dockletRequest.post(\"/batch/job/output/\",data,masterip)\n    return json.dumps(result)\n\n@app.route(\"/workspace/create/\", methods=['GET'])\n#@activated_required\ndef addCluster():\n    return addClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/list/\", methods=['GET'])\n@login_required\ndef listCluster(masterip):\n    listClusterView.masterip = masterip\n    return listClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/add/\", methods=['POST'])\n@login_required\ndef createCluster(masterip):\n    createClusterView.clustername = request.form[\"clusterName\"]\n    createClusterView.image = request.form[\"image\"]\n    createClusterView.masterip = masterip\n    return createClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/scaleout/<clustername>/\", methods=['POST'])\n@login_required\ndef scaleout(clustername,masterip):\n    scaleoutView.image = request.form[\"image\"]\n    scaleoutView.masterip = masterip\n    scaleoutView.clustername = clustername\n    return scaleoutView.as_view()\n\n@app.route(\"/workspace/<masterip>/scalein/<clustername>/<containername>/\", methods=['GET'])\n@login_required\ndef scalein(clustername,containername,masterip):\n    scaleinView.clustername = clustername\n    scaleinView.containername = containername\n    scaleinView.masterip = masterip\n    return scaleinView.as_view()\n\n@app.route(\"/workspace/<masterip>/start/<clustername>/\", methods=['GET'])\n@login_required\ndef startClustet(clustername,masterip):\n    startClusterView.clustername = clustername\n    startClusterView.masterip = masterip\n    return startClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/stop/<clustername>/\", methods=['GET'])\n@login_required\ndef stopClustet(clustername,masterip):\n    stopClusterView.clustername = clustername\n    stopClusterView.masterip = masterip\n    return stopClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/delete/<clustername>/\", methods=['GET'])\n@login_required\ndef deleteClustet(clustername,masterip):\n    deleteClusterView.clustername = clustername\n    deleteClusterView.masterip = masterip\n    return deleteClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/detail/<clustername>/\", methods=['GET'])\n@login_required\ndef detailCluster(clustername,masterip):\n    detailClusterView.clustername = clustername\n    detailClusterView.masterip = masterip\n    return detailClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/flush/<clustername>/<containername>/\", methods=['GET'])\n@login_required\ndef flushCluster(clustername,containername):\n    flushClusterView.clustername = clustername\n    flushClusterView.containername = containername\n    return flushClusterView.as_view()\n\n@app.route(\"/workspace/<masterip>/save/<clustername>/<containername>/\", methods=['POST'])\n@login_required\ndef saveImage(clustername,containername,masterip):\n    saveImageView.clustername = clustername\n    saveImageView.containername = containername\n    saveImageView.masterip = masterip\n    saveImageView.isforce = \"false\"\n    saveImageView.imagename = request.form['ImageName']\n    saveImageView.description = request.form['description']\n    return saveImageView.as_view()\n\n@app.route(\"/workspace/<masterip>/save/<clustername>/<containername>/force/\", methods=['POST'])\n@login_required\ndef saveImage_force(clustername,containername,masterip):\n    saveImageView.clustername = clustername\n    saveImageView.containername = containername\n    saveImageView.masterip = masterip\n    saveImageView.isforce = \"true\"\n    saveImageView.imagename = request.form['ImageName']\n    saveImageView.description = request.form['description']\n    return saveImageView.as_view()\n\n'''@app.route(\"/addproxy/<masterip>/<clustername>/\", methods=['POST'])\n@login_required\ndef addproxy(clustername,masterip):\n    addproxyView.clustername = clustername\n    addproxyView.masterip = masterip\n    addproxyView.ip = request.form['proxy_ip']\n    addproxyView.port = request.form['proxy_port']\n    return addproxyView.as_view()'''\n\n'''@app.route(\"/deleteproxy/<masterip>/<clustername>/\", methods=['GET'])\n@login_required\ndef deleteproxy(clustername,masterip):\n    deleteproxyView.clustername = clustername\n    deleteproxyView.masterip = masterip\n    return deleteproxyView.as_view()'''\n\n@app.route(\"/port_mapping/add/<masterip>/\", methods=['POST'])\n@login_required\ndef addPortMapping(masterip):\n    addPortMappingView.masterip = masterip\n    return addPortMappingView.as_view()\n\n@app.route(\"/port_mapping/delete/<masterip>/<clustername>/<node_name>/<node_port>/\", methods=['GET'])\n@login_required\ndef delPortMapping(masterip,clustername,node_name,node_port):\n    delPortMappingView.masterip = masterip\n    delPortMappingView.clustername = clustername\n    delPortMappingView.node_name = node_name\n    delPortMappingView.node_port = node_port\n    return delPortMappingView.as_view()\n\n@app.route(\"/getmasterdesc/<mastername>/\", methods=['POST'])\n@login_required\ndef getmasterdesc(mastername):\n    return env.getenv(mastername+\"_desc\")[1:-1]\n\n@app.route(\"/masterdesc/<mastername>/\", methods=['GET'])\n@login_required\ndef masterdesc(mastername):\n    descriptionMasterView.desc=env.getenv(mastername+\"_desc\")[1:-1]\n    return descriptionMasterView.as_view()\n\n\n@app.route(\"/image/<masterip>/list/\", methods=['POST'])\n@login_required\ndef image_list(masterip):\n    data = {\n        \"user\": session['username']\n    }\n#    path = request.path[:request.path.rfind(\"/\")]\n#    path = path[:path.rfind(\"/\")+1]\n    result = dockletRequest.post(\"/image/list/\", data, masterip)\n    logger.debug(\"image\" + str(type(result)))\n    return json.dumps(result)\n\n@app.route(\"/image/<masterip>/description/<image>/\", methods=['GET'])\n@login_required\ndef descriptionImage(image,masterip):\n    descriptionImageView.image = image\n    descriptionImageView.masterip = masterip\n    return descriptionImageView.as_view()\n\n@app.route(\"/image/<masterip>/share/<image>/\", methods=['GET'])\n@login_required\ndef shareImage(image,masterip):\n    shareImageView.image = image\n    shareImageView.masterip = masterip\n    return shareImageView.as_view()\n\n@app.route(\"/image/<masterip>/unshare/<image>/\", methods=['GET'])\n@login_required\ndef unshareImage(image,masterip):\n    unshareImageView.image = image\n    unshareImageView.masterip = masterip\n    return unshareImageView.as_view()\n\n@app.route(\"/image/<masterip>/delete/<image>/\", methods=['GET'])\n@login_required\ndef deleteImage(image,masterip):\n    deleteImageView.image = image\n    deleteImageView.masterip = masterip\n    return deleteImageView.as_view()\n\n@app.route(\"/image/<masterip>/copy/<image>/\", methods=['POST'])\n@login_required\ndef copyImage(image,masterip):\n    copyImageView.image = image\n    copyImageView.masterip = masterip\n    copyImageView.target = request.form['target']\n    return copyImageView.as_view()\n\n\n@app.route(\"/image/<masterip>/updatebase/<image>/\", methods=['GET'])\n@login_required\ndef updatebaseImage(image,masterip):\n    updatebaseImageView.image = image\n    updatebaseImageView.masterip = masterip\n    return updatebaseImageView.as_view()\n\n@app.route(\"/hosts/\", methods=['GET'])\n@administration_required\ndef hosts():\n    return hostsView.as_view()\n\n@app.route(\"/hosts/<masterip>/migrate/<hostip>/\", methods=['POST'])\n@administration_required\ndef hostMigrate(hostip, masterip):\n    hostMigrateView.hostip = hostip\n    hostMigrateView.masterip = masterip\n    hostMigrateView.target = request.form.getlist('target')\n    return hostMigrateView.as_view()\n\n@app.route(\"/hosts/<masterip>/<com_ip>/\", methods=['GET'])\n@administration_required\ndef hostsRealtime(com_ip,masterip):\n    hostsRealtimeView.com_ip = com_ip\n    hostsRealtimeView.masterip = masterip\n    return hostsRealtimeView.as_view()\n\n@app.route(\"/hosts/<masterip>/<com_ip>/containers/\", methods=['GET'])\n@administration_required\ndef hostsConAll(com_ip,masterip):\n    hostsConAllView.com_ip = com_ip\n    hostsConAllView.masterip = masterip\n    return hostsConAllView.as_view()\n\n@app.route(\"/hosts/<masterip>/<com_ip>/containers/<node_name>/\", methods=['GET'])\n@administration_required\ndef hostsConRealtime(com_ip,node_name,masterip):\n    statusRealtimeView.masterip = masterip\n    statusRealtimeView.node_name = node_name\n    return statusRealtimeView.as_view()\n\n@app.route(\"/vclusters/\", methods=['GET'])\n@login_required\ndef status():\n    return statusView.as_view()\n\n@app.route(\"/vclusters/<masterip>/<vcluster_name>/<node_name>/\", methods=['GET'])\n@login_required\ndef statusRealtime(vcluster_name,node_name,masterip):\n    statusRealtimeView.masterip = masterip\n    statusRealtimeView.node_name = node_name\n    return statusRealtimeView.as_view()\n\n@app.route(\"/history/\", methods=['GET'])\n#@login_required\ndef history():\n    return historyView.as_view()\n\n\n@app.route(\"/history/<masterip>/<vnode_name>/\", methods=['GET'])\n@login_required\ndef historyVNode(vnode_name,masterip):\n    historyVNodeView.masterip = masterip\n    historyVNodeView.vnode_name = vnode_name\n    return historyVNodeView.as_view()\n\n@app.route(\"/monitor/<masterip>/hosts/<comid>/<infotype>/\", methods=['POST'])\n@app.route(\"/monitor/<masterip>/vnodes/<comid>/<infotype>/\", methods=['POST'])\n@login_required\ndef monitor_request(comid,infotype,masterip):\n    data = {\n        \"user\": session['username']\n    }\n    path = request.path[request.path.find(\"/\")+1:]\n    path = path[path.find(\"/\")+1:]\n    path = path[path.find(\"/\")+1:]\n    logger.debug(path + \"_____\" + masterip)\n    result = dockletRequest.post(\"/monitor/\"+path, data, masterip)\n    logger.debug(\"monitor\" + str(type(result)))\n    return json.dumps(result)\n\n@app.route(\"/monitor/<masterip>/user/<issue>/\", methods=['POST'])\n@login_required\ndef monitor_user_request(issue,masterip):\n    data = {\n        \"user\": session['username']\n    }\n    path = \"/monitor/user/\" + str(issue) + \"/\"\n    logger.debug(path + \"_____\" + masterip)\n    result = dockletRequest.post(path, data, masterip)\n    logger.debug(\"monitor\" + str(type(result)))\n    return json.dumps(result)\n\n@app.route(\"/beans/application/\", methods=['GET'])\n@login_required\ndef beansapplication():\n    return beansapplicationView.as_view()\n\n@app.route(\"/beans/apply/\", methods=['POST'])\n@login_required\ndef beansapply():\n    return beansapplyView.as_view()\n\n@app.route(\"/beans/admin/<username>/<msgid>/<cmd>/\", methods=['GET'])\n@login_required\n@administration_required\ndef beansadmin(username,msgid,cmd):\n    beansadminView.msgid = msgid\n    beansadminView.username = username\n    if cmd == \"agree\" or cmd == \"reject\":\n        beansadminView.cmd = cmd\n        return beansadminView.as_view()\n    else:\n        return redirect(\"/user/list/\")\n\n'''@app.route(\"/monitor/User/\", methods=['GET'])\n@administration_required\ndef monitorUserAll():\n    return monitorUserAllView.as_view()\n'''\n\n@app.route(\"/logs/\", methods=['GET', 'POST'])\n@administration_required\ndef logs():\n    return logsView.as_view()\n\n@app.route(\"/logs/<filename>/\", methods=['GET'])\n@administration_required\ndef logs_get(filename):\n    data = {\n            \"filename\": filename\n    }\n    result = dockletRequest.post('/logs/get/', data).get('result', '')\n    response = make_response(result)\n    response.headers[\"content-type\"] = \"text/plain\"\n    return response\n\n@app.route(\"/user/list/\", methods=['GET', 'POST'])\n@administration_required\ndef userlist():\n    return userlistView.as_view()\n\n@app.route(\"/user/lock/release/<ulockname>/\", methods=['GET', 'POST'])\n@administration_required\ndef userLockRelease(ulockname):\n    data = {\n        \"ulockname\": ulockname\n    }\n    result = dockletRequest.post_to_all(\"/admin/ulock/release/\", data)\n    #logger.debug(result)\n    return json.dumps(result)\n\n@app.route(\"/group/list/\", methods=['POST'])\n@administration_required\ndef grouplist():\n    return grouplistView.as_view()\n\n@app.route(\"/group/detail/\", methods=['POST'])\n@administration_required\ndef groupdetail():\n    return groupdetailView.as_view()\n\n@app.route(\"/group/query/\", methods=['POST'])\n@administration_required\ndef groupquery():\n    return groupqueryView.as_view()\n\n@app.route(\"/group/modify/<groupname>/\", methods=['POST'])\n@administration_required\ndef groupmodify(groupname):\n    return groupmodifyView.as_view()\n\n@app.route(\"/user/data/\", methods=['GET', 'POST'])\n@administration_required\ndef userdata():\n    return userdataView.as_view()\n\n@app.route(\"/user/add/\", methods=['POST'])\n@administration_required\ndef useradd():\n    return useraddView.as_view()\n\n@app.route(\"/user/modify/\", methods=['POST'])\n@administration_required\ndef usermodify():\n    return usermodifyView.as_view()\n\n@app.route(\"/user/change/\", methods=['POST'])\n@administration_required\ndef userchange():\n    return usermodifyView.as_view()\n\n@app.route(\"/quota/add/\", methods=['POST'])\n@administration_required\ndef quotaadd():\n    return quotaaddView.as_view()\n\n@app.route(\"/quota/chdefault/\", methods=['POST'])\n@administration_required\ndef chdefault():\n    return chdefaultView.as_view()\n\n@app.route(\"/quota/chlxcsetting/\", methods=['POST'])\n@administration_required\ndef chlxcsetting():\n    return chlxcsettingView.as_view()\n\n@app.route(\"/group/add/\", methods=['POST'])\n@administration_required\ndef groupadd():\n    return groupaddView.as_view()\n\n@app.route(\"/group/delete/<groupname>/\", methods=['POST', 'GET'])\n@administration_required\ndef groupdel(groupname):\n    groupdelView.groupname = groupname\n    return groupdelView.as_view()\n\n@app.route(\"/user/info/\", methods=['GET', 'POST'])\n@login_required\ndef userinfo():\n    return userinfoView.as_view()\n\n@app.route(\"/user/selfQuery/\", methods=['GET', 'POST'])\n@login_required\ndef userselfQuery():\n    result = dockletRequest.post('/user/selfQuery/')\n    return json.dumps(result['data'])\n\n@app.route(\"/user/query/\", methods=['GET', 'POST'])\n@administration_required\ndef userquery():\n    return userqueryView.as_view()\n\n@app.route(\"/cloud/\", methods=['GET', 'POST'])\n@administration_required\ndef cloud():\n    return cloudView.as_view()\n\n@app.route(\"/cloud/<masterip>/setting/modify/\", methods = ['POST'])\n@administration_required\ndef cloud_setting_modify(masterip):\n    cloudSettingModifyView.masterip = masterip\n    return cloudSettingModifyView.as_view()\n\n@app.route(\"/cloud/<masterip>/node/add/\", methods = ['POST', 'GET'])\n@administration_required\ndef cloud_node_add(masterip):\n    cloudNodeAddView.masterip = masterip\n    return cloudNodeAddView.as_view()\n\n\n@app.route(\"/notification/\", methods=['GET'])\n@administration_required\ndef notification_list():\n    return NotificationView.as_view()\n\n\n@app.route(\"/notification/create/\", methods=['GET', 'POST'])\n@administration_required\ndef create_notification():\n    return CreateNotificationView.as_view()\n\n\n@app.route(\"/notification/modify/\", methods=['POST'])\n@administration_required\ndef modify_notification():\n    return ModifyNotificationView.as_view()\n\n\n@app.route(\"/notification/delete/\", methods=['POST'])\n@administration_required\ndef delete_notification():\n    return DeleteNotificationView.as_view()\n\n\n@app.route(\"/notification/query_self/\", methods=['POST'])\n@login_required\ndef query_self_notifications():\n    return QuerySelfNotificationsView.as_view()\n\n\n@app.route(\"/notification/detail/<notify_id>/\", methods=['GET'])\n@login_required\ndef query_notification_detail(notify_id):\n    return QueryNotificationView.get_by_id(notify_id)\n\n\n@app.route(\"/system/modify/\", methods=['POST'])\n@administration_required\ndef systemmodify():\n    return systemmodifyView.as_view()\n\n@app.route(\"/system/clear_history/\", methods=['POST'])\n@administration_required\ndef systemclearhistory():\n    return systemclearView.as_view()\n\n@app.route(\"/system/add/\", methods=['POST'])\n@administration_required\ndef systemadd():\n    return systemaddView.as_view()\n\n@app.route(\"/system/delete/\", methods=['POST'])\n@administration_required\ndef systemdelete():\n    return systemdeleteView.as_view()\n\n@app.route(\"/system/resetall/\", methods=['POST'])\n@administration_required\ndef systemresetall():\n    return systemresetallView.as_view()\n\n@app.route(\"/settings/\", methods=['GET', 'POST'])\n@administration_required\ndef adminpage():\n    return adminView.as_view()\n\n@app.route(\"/settings/update/\", methods=['POST'])\n@administration_required\ndef updatesettings():\n    return updatesettingsView.as_view()\n\n@app.route('/index/', methods=['GET'])\ndef jupyter_control():\n    return redirect('/dashboard/')\n\n# for download basefs.tar.bz\n# remove, not the function of docklet\n# should download it from a http server\n#@app.route('/download/basefs', methods=['GET'])\n#def download():\n    #fsdir = env.getenv(\"FS_PREFIX\")\n    #return send_from_directory(fsdir+'/local', 'basefs.tar.bz', as_attachment=True)\n\n# jupyter auth APIs\n@app.route('/jupyter/', methods=['GET'])\ndef jupyter_prefix():\n    path = request.args.get('next')\n    if path == None:\n        return redirect('/login/')\n    return redirect('/login/'+'?next='+path)\n\n@app.route('/jupyter/home/', methods=['GET'])\ndef jupyter_home():\n    return redirect('/dashboard/')\n\n@app.route('/jupyter/login/', methods=['GET', 'POST'])\ndef jupyter_login():\n    return redirect('/login/')\n\n@app.route('/jupyter/logout/', methods=['GET'])\ndef jupyter_logout():\n    return redirect('/logout/')\n\n@app.route('/jupyter/authorizations/cookie/<cookie_name>/<cookie_content>/', methods=['GET'])\ndef jupyter_auth(cookie_name, cookie_content):\n    username = cookie_tool.parse_cookie(cookie_content, app.secret_key)\n    if username == None:\n        resp = make_response('cookie auth failed')\n        resp.status_code = 404\n        return resp\n    return json.dumps({'name': username})\n\n@app.errorhandler(401)\ndef not_authorized(error):\n    if \"username\" in session:\n        if \"401\" in session:\n            reason = session['401']\n            session.pop('401', None)\n            if (reason == 'Token Expired'):\n                return redirect('/logout/')\n        return render_template('error/401.html', mysession = session)\n    else:\n        return redirect('/login/')\n\n@app.errorhandler(500)\ndef internal_server_error(error):\n    logger.error(error)\n    logger.error(traceback.format_exc())\n    if \"username\" in session:\n        if \"500\" in session and \"500_title\" in session:\n            reason = session['500']\n            title = session['500_title']\n            session.pop('500', None)\n            session.pop('500_title', None)\n        else:\n            reason = '''The server encountered something unexpected that didn't allow it to complete the request. We apologize.You can go back to\n<a href=\"/dashboard/\">dashboard</a> or <a href=\"/logout\">log out</a>'''\n            title = 'Internal Server Error'\n        return render_template('error/500.html', mysession = session, reason = reason, title = title)\n    else:\n        return redirect('/login/')\nif __name__ == '__main__':\n    '''\n    to generate a secret_key\n\n    from base64 import b64encode\n    from os import urandom\n\n    secret_key = urandom(24)\n    secret_key = b64encode(secret_key).decode('utf-8')\n\n    '''\n    logger.info('Start Flask...:')\n    try:\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/web_secret_key.txt')\n        app.secret_key = secret_key_file.read()\n        secret_key_file.close()\n    except:\n        from base64 import b64encode\n        from os import urandom\n        secret_key = urandom(24)\n        secret_key = b64encode(secret_key).decode('utf-8')\n        app.secret_key = secret_key\n        secret_key_file = open(env.getenv('FS_PREFIX') + '/local/web_secret_key.txt', 'w')\n        secret_key_file.write(secret_key)\n        secret_key_file.close()\n\n    try:\n        open_registryfile = open(env.getenv('FS_PREFIX') + '/local/settings.conf')\n        settings = json.loads(open_registryfile.read())\n        open_registryfile.close()\n        os.environ['OPEN_REGISTRY'] = settings.get('OPEN_REGISTRY',\"False\")\n    except:\n        os.environ['OPEN_REGISTRY'] = \"False\"\n\n    os.environ['APP_KEY'] = app.secret_key\n    runcmd = sys.argv[0]\n    app.runpath = runcmd.rsplit('/', 1)[0]\n\n    webip = \"0.0.0.0\"\n    webport = env.getenv(\"WEB_PORT\")\n\n    webViews.dockletrequest.endpoint = 'http://%s:%d' % (env.getenv('MASTER_IP'), env.getenv('MASTER_PORT'))\n\n\n    try:\n        opts, args = getopt.getopt(sys.argv[1:], \"i:p:\", [\"ip=\", \"port=\"])\n    except getopt.GetoptError:\n        print (\"%s -i ip -p port\" % sys.argv[0])\n        sys.exit(2)\n    for opt, arg in opts:\n        if opt in (\"-i\", \"--ip\"):\n            webip = arg\n        elif opt in (\"-p\", \"--port\"):\n            webport = int(arg)\n\n    # Set True when using ssl/https\n    #app.config['SESSION_COOKIE_SECURE'] = True\n    app.run(host = webip, port = webport, threaded=True)\n"
  },
  {
    "path": "web/webViews/admin.py",
    "content": "from flask import session, render_template, redirect, request\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.dashboard import *\nimport time, re, json, os\n\nclass adminView(normalView):\n    template_path = \"settings.html\"\n\n    @classmethod\n    def get(self):\n        result = dockletRequest.post('/user/groupList/')\n        groups = result[\"groups\"]\n        quotas = result[\"quotas\"]\n        defaultgroup = result[\"default\"]\n        parms = dockletRequest.post('/system/parmList/')\n        rootimage = dockletRequest.post('/image/list/').get('images')\n        lxcsetting = dockletRequest.post('/user/lxcsettingList/')['data']\n        settings = dockletRequest.post('/settings/list/')['result']\n        return self.render(self.template_path, groups = groups, quotas = quotas, defaultgroup = defaultgroup, parms = parms, lxcsetting = lxcsetting, root_image = rootimage['private'], settings=settings)\n\nclass updatesettingsView(normalView):\n    \n    @classmethod\n    def post(self):\n        result = dockletRequest.post(\"/settings/update/\", request.form)\n        os.environ['OPEN_REGISTRY'] = request.form.get('OPEN_REGISTRY')\n        return redirect('/settings/')\n\nclass groupaddView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/user/groupadd/', request.form)\n        return redirect('/settings/')\n\nclass systemmodifyView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/modify/', request.form)\n        return redirect('/settings/')\n\nclass systemclearView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/clear_history/', request.form)\n        return redirect('/settings/')\n\nclass systemaddView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/add/', request.form)\n        return redirect('/settings/')\n\nclass systemdeleteView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/delete/', request.form)\n        return redirect('/settings/')\n\nclass systemresetallView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/reset_all/', request.form)\n        return redirect('/settings/')\n\nclass quotaaddView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/user/quotaadd/', request.form)\n        return redirect('/settings/')\n\nclass chdefaultView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/user/chdefault/', request.form)\n        return redirect('/settings/')\n\nclass chlxcsettingView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/user/chlxcsetting/', request.form)\n        return redirect('/settings/')\n\nclass groupdelView(normalView):\n    @classmethod\n    def post(self):\n        data = {\n                \"name\" : self.groupname,\n        }\n        dockletRequest.post('/user/groupdel/', data)\n        return redirect('/settings/')\n\n    @classmethod\n    def get(self):\n        return self.post()\n\nclass chparmView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/chparm/', request.form)\n\nclass historydelView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/system/historydel/', request.form)\n        return redirect('/settings/')\n\nclass updatebaseImageView(normalView):\n    @classmethod\n    def get(self):\n        data = {\n                \"image\": self.image\n        }\n        dockletRequest.post('/image/updatebase/', data)\n        return redirect(\"/settings/\")\n\nclass hostMigrateView(normalView):\n    @classmethod\n    def post(self):\n        data = {\n                \"src_host\": self.hostip,\n                \"dst_host_list\": self.target\n        }\n        dockletRequest.post(\"/host/migrate/\", data, self.masterip)\n        return redirect(\"/hosts/\")\n"
  },
  {
    "path": "web/webViews/authenticate/auth.py",
    "content": "from flask import session, request, abort, redirect\nfrom functools import wraps\n\n\ndef login_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if request.method == 'POST' :\n            if not is_authenticated():\n                abort(401)\n            else:\n                return func(*args, **kwargs)\n        else:\n            if not is_authenticated():\n                return redirect(\"/login/\" + \"?next=\" + request.path)\n            else:\n                return func(*args, **kwargs)\n\n    return wrapper\n\ndef administration_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if not is_admin():\n            abort(401)\n        else:\n            return func(*args, **kwargs)\n\n\n    return wrapper\n\ndef activated_required(func):\n    @wraps(func)\n    def wrapper(*args, **kwargs):\n        if not is_activated():\n            abort(401)\n        else:\n            return func(*args, **kwargs)\n\n\n    return wrapper\n\ndef is_authenticated():\n    if \"username\" in session:\n        return True\n    else:\n        return False\ndef is_admin():\n    if not \"username\" in session:\n        return False\n    if not (session['usergroup'] == 'root' or session['usergroup'] == 'admin'):\n        return False\n    return True\n\ndef is_activated():\n    if not \"username\" in session:\n        return False\n    if not (session['status']=='normal'):\n        return False\n    return True\n"
  },
  {
    "path": "web/webViews/authenticate/login.py",
    "content": "from webViews.view import normalView\nfrom webViews.authenticate.auth import is_authenticated\nfrom webViews.dockletrequest import dockletRequest\nfrom flask import redirect, request, render_template, session, make_response, abort\nfrom webViews import cookie_tool\n\nimport hashlib\n#from suds.client import Client\n\nimport os, sys, inspect\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))\nsrc_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,\"../../..\", \"src\")))\nif src_folder not in sys.path:\n    sys.path.insert(0, src_folder)\n\nfrom utils import env\n\nif (env.getenv('EXTERNAL_LOGIN') == 'True'):\n    sys.path.insert(0, os.path.realpath(os.path.abspath(os.path.join(this_folder,\"../../../src\", \"plugin\"))))\n    import external_generate\n\ndef refreshInfo():\n    data = {}\n    result = dockletRequest.post('/user/selfQuery/', data)\n    ok = result and result.get('success', None)\n    if (ok and ok == \"true\"):\n        session['username'] = result['data']['username']\n        session['nickname'] = result['data']['nickname']\n        session['description'] = result['data']['description']\n        session['avatar'] = '/static/avatar/'+ result['data']['avatar']\n        session['usergroup'] = result['data']['group']\n        session['status'] = result['data']['status']\n    else:\n        abort(404)\n\nclass loginView(normalView):\n    template_path = \"login.html\"\n\n    @classmethod\n    def get(self):\n        if is_authenticated():\n            refreshInfo()\n            return redirect(request.args.get('next',None) or '/dashboard/')\n        if (env.getenv('EXTERNAL_LOGIN') == 'True'):\n            url = external_generate.external_login_url\n            link = external_generate.external_login_link\n        else:\n            link = ''\n            url = ''\n        return render_template(self.template_path, loginMsg=\"\", link = link, url = url, open_registry=self.open_registry)\n\n    @classmethod\n    def post(self):\n        if (request.form['username']):\n            data = {\"user\": request.form['username'], \"key\": request.form['password'], 'ip': request.remote_addr}\n            result = dockletRequest.unauthorizedpost('/login/', data)\n            ok = result and result.get('success', None)\n            if (ok and (ok == \"true\")):\n                # set cookie:docklet-jupyter-cookie for jupyter notebook\n                resp = make_response(redirect(request.args.get('next',None) or '/dashboard/'))\n                app_key = os.environ['APP_KEY']\n                resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(request.form['username'], app_key))\n                # set session for docklet\n                session['username'] = request.form['username']\n                session['nickname'] = result['data']['nickname']\n                session['description'] = result['data']['description']\n                session['avatar'] = '/static/avatar/'+ result['data']['avatar']\n                session['usergroup'] = result['data']['group']\n                session['status'] = result['data']['status']\n                session['token'] = result['data']['token']\n                return resp\n            else:\n                if (env.getenv('EXTERNAL_LOGIN') == 'True'):\n                    url = external_generate.external_login_url\n                    link = external_generate.external_login_link\n                else:\n                    link = ''\n                    url = ''\n                loginMsg = result.get('message', '')\n                return render_template(self.template_path, loginMsg=loginMsg, link = link, url = url, open_registry=self.open_registry)\n        else:\n            return redirect('/login/')\n\nclass logoutView(normalView):\n\n    @classmethod\n    def get(self):\n        resp = make_response(redirect('/login/'))\n        session.pop('username', None)\n        session.pop('nickname', None)\n        session.pop('description', None)\n        session.pop('avatar', None)\n        session.pop('status', None)\n        session.pop('usergroup', None)\n        session.pop('token', None)\n        resp.set_cookie('docklet-jupyter-cookie', '', expires=0)\n        return resp\n\n\nclass external_login_callbackView(normalView):\n    @classmethod\n    def get(self):\n\n        form = external_generate.external_auth_generate_request()\n        result = dockletRequest.unauthorizedpost('/external_login/', form)\n        ok = result and result.get('success', None)\n        if (ok and (ok == \"true\")):\n            # set cookie:docklet-jupyter-cookie for jupyter notebook\n            resp = make_response(redirect(request.args.get('next',None) or '/dashboard/'))\n            app_key = os.environ['APP_KEY']\n            resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(result['data']['username'], app_key))\n            # set session for docklet\n            session['username'] = result['data']['username']\n            session['nickname'] = result['data']['nickname']\n            session['description'] = result['data']['description']\n            session['avatar'] = '/static/avatar/'+ result['data']['avatar']\n            session['usergroup'] = result['data']['group']\n            session['status'] = result['data']['status']\n            session['token'] = result['data']['token']\n            return resp\n        else:\n            return redirect('/login/')\n\n    @classmethod\n    def post(self):\n\n        form = external_generate.external_auth_generate_request()\n        result = dockletRequest.unauthorizedpost('/external_login/', form)\n        ok = result and result.get('success', None)\n        if (ok and (ok == \"true\")):\n            # set cookie:docklet-jupyter-cookie for jupyter notebook\n            resp = make_response(redirect(request.args.get('next',None) or '/dashboard/'))\n            app_key = os.environ['APP_KEY']\n            resp.set_cookie('docklet-jupyter-cookie', cookie_tool.generate_cookie(result['data']['username'], app_key))\n            # set session for docklet\n            session['username'] = result['data']['username']\n            session['nickname'] = result['data']['nickname']\n            session['description'] = result['data']['description']\n            session['avatar'] = '/static/avatar/'+ result['data']['avatar']\n            session['usergroup'] = result['data']['group']\n            session['status'] = result['data']['status']\n            session['token'] = result['data']['token']\n            return resp\n        else:\n            return redirect('/login/')\n\nclass external_loginView(normalView):\n    if (env.getenv('EXTERNAL_LOGIN') == 'True'):\n        template_path = external_generate.html_path\n\n    @classmethod\n    def post(self):\n        return render_template(self.template_path)\n\n    @classmethod\n    def get(self):\n        return self.post()\n"
  },
  {
    "path": "web/webViews/authenticate/register.py",
    "content": "from webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\nfrom flask import redirect, request, abort, render_template\n\nclass registerView(normalView):\n    template_path = 'register.html'\n\n    @classmethod\n    def post(self):\n        form = dict(request.form)\n        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):\n            abort(500)\n        result = dockletRequest.unauthorizedpost('/register/', form)\n        return redirect(\"/login/\")\n\n    @classmethod\n    def get(self):\n        return render_template(self.template_path)\n"
  },
  {
    "path": "web/webViews/batch.py",
    "content": "from flask import session, redirect, request\nfrom webViews.view import normalView\nfrom webViews.log import logger\nfrom webViews.checkname import checkname\nfrom webViews.dockletrequest import dockletRequest\nfrom utils import env\nimport json\n\nclass batchAdminListView(normalView):\n    template_path = \"batch/batch_admin_list.html\"\n\n    @classmethod\n    def get(self):\n        masterips = dockletRequest.post_to_all()\n        job_list = {}\n        for ipname in masterips:\n            ip = ipname.split(\"@\")[0]\n            result = dockletRequest.post(\"/batch/job/listall/\",{},ip)\n            job_list[ip] = result.get(\"data\")\n            logger.debug(\"job_list[%s]: %s\" % (ip,job_list[ip]))\n        if True:\n            return self.render(self.template_path, masterips=masterips, job_list=job_list)\n        else:\n            return self.error()\n\nclass batchJobListView(normalView):\n    template_path = \"batch/batch_list.html\"\n\n    @classmethod\n    def get(self):\n        masterips = dockletRequest.post_to_all()\n        job_list = {}\n        for ipname in masterips:\n            ip = ipname.split(\"@\")[0]\n            result = dockletRequest.post(\"/batch/job/list/\",{},ip)\n            job_list[ip] = result.get(\"data\")\n            logger.debug(\"job_list[%s]: %s\" % (ip,job_list[ip]))\n        if True:\n            return self.render(self.template_path, masterips=masterips, job_list=job_list)\n        else:\n            return self.error()\n\nclass createBatchJobView(normalView):\n    template_path = \"batch/batch_create.html\"\n\n    @classmethod\n    def get(self):\n        masterips = dockletRequest.post_to_all()\n        images = {}\n        for master in masterips:\n            images[master.split(\"@\")[0]] = dockletRequest.post(\"/image/list/\",{},master.split(\"@\")[0]).get(\"images\")\n        logger.info(images)\n\n        data = {\n            \"user\": session['username'],\n        }\n        allresult = dockletRequest.post_to_all('/monitor/listphynodes/', data)\n        allmachines = {}\n        for master in allresult:\n            allmachines[master.split(\"@\")[0]] = []\n            iplist = allresult[master].get('monitor').get('allnodes')\n            for ip in iplist:\n                result = dockletRequest.post('/monitor/hosts/%s/gpuinfo/'%(ip), data, master.split(\"@\")[0])\n                gpuinfo = result.get('monitor').get('gpuinfo')\n                allmachines[master.split(\"@\")[0]].append(gpuinfo)\n\n        pending_gpu_tasks = {}\n        for master in masterips:\n            pending_gpu_tasks[master.split(\"@\")[0]] = dockletRequest.post(\"/monitor/pending_gpu_tasks/\",{},master.split(\"@\")[0]).get(\"monitor\").get(\"pending_tasks\")\n\n        return self.render(self.template_path, user=session['username'], masterips=masterips, images=images, allmachines=allmachines, pending_gpu_tasks=pending_gpu_tasks)\n\n\nclass infoBatchJobView(normalView):\n    template_path = \"batch/batch_info.html\"\n    error_path = \"error.html\"\n    masterip = \"\"\n    jobid = \"\"\n\n    @classmethod\n    def get(self):\n        data = {\n            'jobid':self.jobid\n        }\n        result = dockletRequest.post(\"/batch/job/info/\",data,self.masterip)\n        data = result.get(\"data\")\n        logger.info(str(data))\n        #logger.debug(\"job_list: %s\" % job_list)\n        if result.get('success',\"\") == \"true\":\n            return self.render(self.template_path, masterip=self.masterip, jobinfo=data)\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass addBatchJobView(normalView):\n    template_path = \"batch/batch_list.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        result = dockletRequest.post(\"/batch/job/add/\", self.job_data, masterip)\n        if result.get('success', None) == \"true\":\n            return redirect('/batch_jobs/')\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass stopBatchJobView(normalView):\n    template_path = \"batch/batch_list.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {'jobid':self.jobid}\n        result = dockletRequest.post(\"/batch/job/stop/\", data, masterip)\n        if result.get('success', None) == \"true\":\n            return redirect('/batch_jobs/')\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass adminStopBatchJobView(normalView):\n    template_path = \"batch/batch_admin_list.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {'jobid':self.jobid}\n        result = dockletRequest.post(\"/batch/job/stop/\", data, masterip)\n        if result.get('success', None) == \"true\":\n            return redirect('/admin_batch_list/')\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass outputBatchJobView(normalView):\n    template_path = \"batch/batch_output.html\"\n    masterip = \"\"\n    jobid = \"\"\n    taskid = \"\"\n    vnodeid = \"\"\n    issue = \"\"\n\n    @classmethod\n    def get(self):\n        data = {\n            'jobid':self.jobid,\n            'taskid':self.taskid,\n            'vnodeid':self.vnodeid,\n            'issue':self.issue\n        }\n        result = dockletRequest.post(\"/batch/job/output/\",data,self.masterip)\n        output = result.get(\"data\")\n        #logger.debug(\"job_list: %s\" % job_list)\n        if result.get('success',\"\") == \"true\":\n            return self.render(self.template_path, masterip=self.masterip, jobid=self.jobid,\n                               taskid=self.taskid, vnodeid=self.vnodeid, issue=self.issue, output=output)\n        else:\n            return self.error()\n"
  },
  {
    "path": "web/webViews/beansapplication.py",
    "content": "from flask import session,render_template,request,redirect\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\n\nclass beansapplicationView(normalView):\n    template_path = \"beansapplication.html\"\n\n    @classmethod\n    def get(self):\n        result = dockletRequest.post('/beans/applymsgs/').get('applymsgs')\n        return self.render(self.template_path, applications = result)\n\n    @classmethod\n    def post(self):\n        return self.get()\n\nclass beansapplyView(normalView):\n    template_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        data = {\"number\":request.form[\"number\"],\"reason\":request.form[\"reason\"]}\n        result = dockletRequest.post('/beans/apply/',data)\n        success = result.get(\"success\")\n        if success == \"true\":\n            return redirect(\"/beans/application/\")\n        else:\n            return self.render(self.template_path, message = result.get(\"message\"))\n\n    @classmethod\n    def get(self):\n        return self.post()\n\nclass beansadminView(normalView):\n    username = \"\"\n    msgid = \"\"\n    cmd = \"\"\n    template_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        data = {\"username\":self.username, \"msgid\":self.msgid}\n        result = dockletRequest.post('/beans/admin/'+self.cmd+\"/\",data)\n        success = result.get(\"success\")\n        if success == \"true\":\n            return redirect(\"/user/list/\")\n        else:\n            return self.render(self.template_path, message = result.get(\"message\"))\n"
  },
  {
    "path": "web/webViews/checkname.py",
    "content": "import re\nfrom flask import abort, session\n\npattern = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')\nerror_msg = ''' Your name may cause errors, Please use names starting with a-z,A-Z or _ and contains only elements in {a-z, A-Z, _, 0-9}\n'''\nerror_title = 'Input Error'\n\ndef checkname(str):\n    try:\n        match = pattern.match(str)\n        if (match == None):\n            session['500'] = error_msg\n            session['500_title'] = error_title\n            abort(500)\n        if (match.group() != str):\n            session['500'] = error_msg\n            session['500_title'] = error_title\n            abort(500)\n        return True\n    except:\n        session['500'] = error_msg\n        session['500_title'] = error_title\n        abort(500)\n"
  },
  {
    "path": "web/webViews/cloud.py",
    "content": "from flask import session, render_template, redirect, request\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\nimport time, re, json\n\n\nclass cloudView(normalView):\n    template_path = \"cloud.html\"\n\n    @classmethod\n    def post(self):\n        settings = dockletRequest.post_to_all('/cloud/setting/get/')\n        return self.render(self.template_path, settings = settings)\n\n    @classmethod\n    def get(self):\n        return self.post()\n\nclass cloudSettingModifyView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/cloud/setting/modify/', request.form, self.masterip)\n        return redirect('/cloud/')\n\nclass cloudNodeAddView(normalView):\n    @classmethod\n    def post(self):\n        data = {}\n        dockletRequest.post('/cloud/node/add/', data, self.masterip)\n        return redirect('/hosts/')\n\n    @classmethod\n    def get(self):\n        return self.post()\n"
  },
  {
    "path": "web/webViews/cluster.py",
    "content": "from flask import session, redirect, request\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.dashboard import *\nfrom webViews.checkname import checkname\nimport time, re\n\nclass addClusterView(normalView):\n    template_path = \"addCluster.html\"\n\n    @classmethod\n    def get(self):\n        masterips = dockletRequest.post_to_all()\n        images = dockletRequest.post(\"/image/list/\",{},masterips[0].split(\"@\")[0]).get(\"images\")\n        desc = dockletRequest.getdesc(masterips[0].split(\"@\")[1])\n        result = dockletRequest.post(\"/user/usageQuery/\")\n        quota = result.get(\"quota\")\n        usage = result.get(\"usage\")\n        default = result.get(\"default\")\n        restcpu = int(quota['cpu']) - int(usage['cpu'])\n        restmemory = int(quota['memory']) - int(usage['memory'])\n        restdisk = int(quota['disk']) - int(usage['disk'])\n        if restcpu >= int(default['cpu']):\n            defaultcpu = default['cpu']\n        elif restcpu <= 0:\n            defaultcpu = \"0\"\n        else:\n            defaultcpu = str(restcpu)\n\n        if restmemory >= int(default['memory']):\n            defaultmemory = default['memory']\n        elif restmemory <= 0:\n            defaultmemory = \"0\"\n        else:\n            defaultmemory = str(restmemory)\n\n        if restdisk >= int(default['disk']):\n            defaultdisk = default['disk']\n        elif restdisk <= 0:\n            defaultdisk = \"0\"\n        else:\n            defaultdisk = str(restdisk)\n\n        defaultsetting = {\n                'cpu': defaultcpu,\n                'memory': defaultmemory,\n                'disk': defaultdisk\n                }\n        if (result):\n            return self.render(self.template_path, user = session['username'],masterips = masterips, images = images, quota = quota, usage = usage, defaultsetting = defaultsetting, masterdesc=desc)\n        else:\n            self.error()\n\nclass createClusterView(normalView):\n    template_path = \"dashboard.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        index1 = self.image.rindex(\"_\")\n        index2 = self.image[:index1].rindex(\"_\")\n        checkname(self.clustername)\n        data = {\n            \"clustername\": self.clustername,\n            'imagename': self.image[:index2],\n            'imageowner': self.image[index2+1:index1],\n            'imagetype': self.image[index1+1:],\n        }\n        result = dockletRequest.post(\"/cluster/create/\", dict(data, **(request.form)), masterip)\n        if(result.get('success', None) == \"true\"):\n           return redirect(\"/dashboard/\")\n            #return self.render(self.template_path, user = session['username'])\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass descriptionMasterView(normalView):\n    template_path = \"description.html\"\n\n    @classmethod\n    def get(self):\n        return self.render(self.template_path, description=self.desc)\n\nclass descriptionImageView(normalView):\n    template_path = \"description.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        index1 = self.image.rindex(\"_\")\n        index2 = self.image[:index1].rindex(\"_\")\n        data = {\n                \"imagename\": self.image[:index2],\n                \"imageowner\": self.image[index2+1:index1],\n                \"imagetype\": self.image[index1+1:]\n        }\n        result = dockletRequest.post(\"/image/description/\", data, masterip)\n        if(result):\n            description = result.get(\"message\")\n            return self.render(self.template_path, description = description)\n        else:\n            self.error()\n\nclass scaleoutView(normalView):\n    error_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        index1 = self.image.rindex(\"_\")\n        index2 = self.image[:index1].rindex(\"_\")\n        data = {\n            \"clustername\": self.clustername,\n            'imagename': self.image[:index2],\n            'imageowner': self.image[index2+1:index1],\n            'imagetype': self.image[index1+1:]\n        }\n        result = dockletRequest.post(\"/cluster/scaleout/\", dict(data, **(request.form)), masterip)\n        if(result.get('success', None) == \"true\"):\n            return redirect(\"/config/\")\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass scaleinView(normalView):\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"clustername\": self.clustername,\n            \"containername\":self.containername\n        }\n        result = dockletRequest.post(\"/cluster/scalein/\", data, masterip)\n        if(result.get('success', None) == \"true\"):\n            return redirect(\"/config/\")\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass listClusterView(normalView):\n    template_path = \"listCluster.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        result = dockletRequest.post(\"/cluster/list/\", {},  masterip)\n        clusters = result.get(\"clusters\")\n        if(result):\n            return self.render(self.template_path, user = session['username'], clusters = clusters)\n        else:\n            self.error()\n\nclass startClusterView(normalView):\n    template_path = \"dashboard.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"clustername\": self.clustername\n        }\n        result = dockletRequest.post(\"/cluster/start/\", data, masterip)\n        if(result.get('success', None) == \"true\"):\n           return redirect(\"/dashboard/\")\n            #return self.render(self.template_path, user = session['username'])\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass stopClusterView(normalView):\n    template_path = \"dashboard.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"clustername\": self.clustername\n        }\n        result = dockletRequest.post(\"/cluster/stop/\", data, masterip)\n        if(result.get('success', None) == \"true\"):\n            return redirect(\"/dashboard/\")\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass flushClusterView(normalView):\n    success_path = \"opsuccess.html\"\n    failed_path = \"opfailed.html\"\n\n    @classmethod\n    def get(self):\n        data = {\n                \"clustername\": self.clustername,\n                \"from_lxc\": self.containername\n        }\n        result = dockletRequest.post(\"/cluster/flush/\", data)\n\n        if(result):\n            if result.get('success') == \"true\":\n                return self.render(self.success_path, user = session['username'])\n            else:\n                return self.render(self.failed_path, user = session['username'])\n        else:\n            self.error()\n\nclass deleteClusterView(normalView):\n    template_path = \"dashboard.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"clustername\": self.clustername\n        }\n        result = dockletRequest.post(\"/cluster/delete/\", data, masterip)\n        if(result.get('success', None) == \"true\"):\n            return redirect(\"/dashboard/\")\n        else:\n            return self.render(self.error_path, message = result.get('message'))\n\nclass detailClusterView(normalView):\n    template_path = \"listcontainer.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"clustername\": self.clustername\n        }\n        result = dockletRequest.post(\"/cluster/info/\", data, masterip)\n        if(result):\n            message = result.get('message')\n            containers = message['containers']\n            status = message['status']\n            return self.render(self.template_path, containers = containers, user = session['username'], clustername = self.clustername, status = status)\n        else:\n            self.error()\n\nclass saveImageView(normalView):\n    template_path = \"saveconfirm.html\"\n    success_path = \"opsuccess.html\"\n    error_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        data = {\n                \"clustername\": self.clustername,\n                \"image\": self.imagename,\n                \"containername\": self.containername,\n                \"description\": self.description,\n                \"isforce\": self.isforce\n        }\n        result = dockletRequest.post(\"/cluster/save/\", data, masterip)\n        if(result):\n            if result.get('success') == 'true':\n                #return self.render(self.success_path, user = session['username'])\n                return redirect(\"/config/\")\n                #res = detailClusterView()\n                #res.clustername = self.clustername\n                #return res.as_view()\n            else:\n                if result.get('reason') == \"exists\":\n                    return self.render(self.template_path, containername = self.containername, clustername = self.clustername, image = self.imagename, user = session['username'], description = self.description, masterip=masterip)\n                else:\n                    return self.render(self.error_path, message = result.get('message'))\n        else:\n            self.error()\n\nclass shareImageView(normalView):\n    template_path = \"dashboard.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"image\": self.image\n        }\n        result = dockletRequest.post(\"/image/share/\", data, masterip)\n        if(result):\n            return redirect(\"/config/\")\n        else:\n            self.error()\n\nclass unshareImageView(normalView):\n    template_path = \"dashboard.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"image\": self.image\n        }\n        result = dockletRequest.post(\"/image/unshare/\", data, masterip)\n        if(result):\n            return redirect(\"/config/\")\n        else:\n            self.error()\n\nclass copyImageView(normalView):\n    error_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        data = {\n                \"image\": self.image,\n                \"target\": self.target\n        }\n        result = dockletRequest.post(\"/image/copy/\", data, masterip)\n        if result:\n            if result.get('success') == 'true':\n                return redirect(\"/config/\")\n            else:\n                return self.render(self.error_path,message=result.get('message'))\n        else:\n            self.error()\n\nclass deleteImageView(normalView):\n    template_path = \"dashboard.html\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n                \"image\": self.image\n        }\n        result = dockletRequest.post(\"/image/delete/\", data, masterip)\n        if(result):\n            return redirect(\"/config/\")\n        else:\n            self.error()\n\nclass addproxyView(normalView):\n\n    @classmethod\n    def post(self):\n        masterip = self.masterip\n        data = {\n            \"clustername\": self.clustername,\n            \"ip\": self.ip,\n            \"port\": self.port\n        }\n        result = dockletRequest.post(\"/addproxy/\", data, masterip)\n        if(result):\n            return redirect(\"/config/\")\n        else:\n            self.error()\n\nclass deleteproxyView(normalView):\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"clustername\":self.clustername\n        }\n        result = dockletRequest.post(\"/deleteproxy/\", data, masterip)\n        if(result):\n            return redirect(\"/config/\")\n        else:\n            self.error()\n\n    @classmethod\n    def post(self):\n        return self.get()\n\nclass configView(normalView):\n    @classmethod\n    def get(self):\n        masterips = dockletRequest.post_to_all()\n        allimages = dockletRequest.post_to_all('/image/list/')\n        for master in allimages:\n            allimages[master] = allimages[master].get('images')\n        allclusters = dockletRequest.post_to_all(\"/cluster/list/\")\n        for master in allclusters:\n            allclusters[master] = allclusters[master].get('clusters')\n        allclusters_info = {}\n        clusters_info = {}\n        data={}\n        for master in allclusters:\n            allclusters_info[master] = {}\n            for cluster in allclusters[master]:\n                data[\"clustername\"] = cluster\n                result = dockletRequest.post(\"/cluster/info/\", data, master.split(\"@\")[0]).get(\"message\")\n                allclusters_info[master][cluster] = result\n        result = dockletRequest.post(\"/user/usageQuery/\")\n        quota = result.get(\"quota\")\n        usage = result.get(\"usage\")\n        default = result.get(\"default\")\n        restcpu = int(quota['cpu']) - int(usage['cpu'])\n        restmemory = int(quota['memory']) - int(usage['memory'])\n        restdisk = int(quota['disk']) - int(usage['disk'])\n        if restcpu >= int(default['cpu']):\n            defaultcpu = default['cpu']\n        elif restcpu <= 0:\n            defaultcpu = \"0\"\n        else:\n            defaultcpu = str(restcpu)\n\n        if restmemory >= int(default['memory']):\n            defaultmemory = default['memory']\n        elif restmemory <= 0:\n            defaultmemory = \"0\"\n        else:\n            defaultmemory = str(restmemory)\n\n        if restdisk >= int(default['disk']):\n            defaultdisk = default['disk']\n        elif restdisk <= 0:\n            defaultdisk = \"0\"\n        else:\n            defaultdisk = str(restdisk)\n\n        defaultsetting = {\n                'cpu': defaultcpu,\n                'memory': defaultmemory,\n                'disk': defaultdisk\n                }\n        return self.render(\"config.html\", allimages = allimages, allclusters = allclusters_info, mysession=dict(session), quota = quota, usage = usage, defaultsetting = defaultsetting, masterips = masterips)\n\n    @classmethod\n    def post(self):\n        return self.get()\n\nclass addPortMappingView(normalView):\n    template_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        data = {\"clustername\":request.form[\"clustername\"],\"node_name\":request.form[\"node_name\"],\"node_ip\":request.form[\"node_ip\"],\"node_port\":request.form[\"node_port\"]}\n        result = dockletRequest.post('/port_mapping/add/',data, self.masterip)\n        success = result.get(\"success\")\n        if success == \"true\":\n            return redirect(\"/config/\")\n        else:\n            return self.render(self.template_path, message = result.get(\"message\"))\n\n    @classmethod\n    def get(self):\n        return self.post()\n\nclass delPortMappingView(normalView):\n    template_path = \"error.html\"\n\n    @classmethod\n    def post(self):\n        data = {\"clustername\":self.clustername,\"node_name\":self.node_name,\"node_port\":self.node_port}\n        result = dockletRequest.post('/port_mapping/delete/',data, self.masterip)\n        success = result.get(\"success\")\n        if success == \"true\":\n            return redirect(\"/config/\")\n        else:\n            return self.render(self.template_path, message = result.get(\"message\"))\n\n    @classmethod\n    def get(self):\n        return self.post()\n"
  },
  {
    "path": "web/webViews/cookie_tool.py",
    "content": "#!/usr/bin/python3\n\nimport json, hashlib, base64, time\nimport sys\nfrom webViews.log import logger\n\n# generate cookie :\n#                name = 'leebaok'\n#                     |\n#   { \"name\":\"leebaok\", \"login-time\":time}             Secure-Key\n#                     |                                    |\n#                     | json.dumps                         |\n#                     |                                    |\n#  '{ \"name\":\"leebaok\", \"login-time\":time}'  ______________|\n#                     |                                    | concat\n#                     | encode('ascii') -> base64          | encode('ascii') -> md5().hexdigest()\n#                     | str(*, encoding='utf-8')           |\n#                     |                                    |\n#      < XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX >.< XXXXXXXXXXXXXXXXXXXX >\n#\n\ndef generate_cookie(name, securekey):\n    #print (\">> generate cookie for %s\" % name)\n    content = { 'name':name, 'login-time': time.asctime() }\n    text = json.dumps(content)\n    part1 = base64.b64encode(text.encode('ascii'))\n    part2 = hashlib.md5( (text+securekey).encode('ascii') ).hexdigest()\n    # part1 is binary(ascii) and part2 is str(utf-8)\n    cookie = str(part1, encoding='utf-8') +\".\"+ part2\n    #print (\"cookie : %s\" % cookie)\n    return cookie\n\ndef parse_cookie(cookie, securekey):\n    logger.info (\">> parse cookie : %s\" % cookie)\n    parts = cookie.split('.')\n    part1 = parts[0]\n    part2 = '' if len(parts) < 2 else parts[1]\n    try:\n        text = str(base64.b64decode(part1.encode('ascii')), encoding='utf-8')\n    except:\n        logger.info (\"decode cookie failed\")\n        return None\n    logger.info (\"cookie content : %s\" % text)\n    thatpart2 = hashlib.md5((text+securekey).encode('ascii')).hexdigest()\n    logger.info (\"hash from part1 : %s\" % thatpart2)\n    logger.info (\"hash from part2 : %s\" % part2)\n    if part2 == thatpart2:\n        result = json.loads(text)['name']\n    else:\n        result = None\n    logger.info (\"parse from cookie : %s\" % result)\n    return result\n"
  },
  {
    "path": "web/webViews/dashboard.py",
    "content": "from flask import session,render_template\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\n\nclass dashboardView(normalView):\n    template_path = \"dashboard.html\"\n\n    @classmethod\n    def get(self):\n        result = dockletRequest.post_to_all('/cluster/list/')\n        desc = dockletRequest.getalldesc()\n        allclusters={}\n        for master in result:\n            clusters = result[master].get(\"clusters\")\n            full_clusters = []\n            data={}\n            for cluster in clusters:\n                data[\"clustername\"] = cluster\n                single_cluster = {}\n                single_cluster['name'] = cluster\n                message = dockletRequest.post(\"/cluster/info/\", data , master.split(\"@\")[0])\n                if(message):\n                    message = message.get(\"message\")\n                    single_cluster['status'] = message['status']\n                    single_cluster['id'] = message['clusterid']\n                    single_cluster['proxy_public_ip'] = message['proxy_public_ip']\n                    full_clusters.append(single_cluster)\n                else:\n                    self.error()\n            allclusters[master] = full_clusters\n        return self.render(self.template_path,  allclusters = allclusters, desc=desc)\n        #else:\n        #    self.error()\n\n    @classmethod\n    def post(self):\n        return self.get()\n"
  },
  {
    "path": "web/webViews/dockletrequest.py",
    "content": "import requests\nfrom flask import abort, session\nfrom webViews.log import logger\nimport os,sys,inspect,traceback\n\n\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe    ()))[0]))\nsrc_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,\"../..\", \"src\")))\nif src_folder not in sys.path:\n    sys.path.insert(0, src_folder)\n\nfrom utils import env\n\nmasterips=env.getenv('MASTER_IPS').split(\",\")\nuser_endpoint = \"http://\" + env.getenv('USER_IP') + \":\" + str(env.getenv('USER_PORT'))\nmaster_port=str(env.getenv('MASTER_PORT'))\n\ndef getip(masterip):\n    return masterip.split(\"@\")[0]\n\ndef getname(masterip):\n    return masterip.split(\"@\")[1]\n\n\nclass dockletRequest():\n\n    @classmethod\n    def post(self, url = '/', data = {}, endpoint = \"http://0.0.0.0:9000\"):\n        #try:\n        data = dict(data)\n        data['token'] = session['token']\n        logger.info (\"Docklet Request: user = %s data = %s, url = %s\"%(session['username'], data, url))\n        reqtype = url.split(\"/\")[1]\n        userreq = {\n                'login',\n                'external_login',\n                'register',\n                'user',\n                'beans',\n                'notification',\n                'settings',\n                'bug'\n                }\n        if \":\" not in endpoint:\n            endpoint = \"http://\"+endpoint+\":\"+master_port\n        if reqtype in userreq:\n            result = requests.post(user_endpoint + url, data=data).json()\n        else:\n            result = requests.post(endpoint + url, data=data).json()\n        # logger.info('response content: %s'%response.content)\n        # result = response.json()\n        if (result.get('success', None) == \"false\" and result.get('reason', None) == \"Unauthorized Action\"):\n            abort(401)\n        if (result.get('Unauthorized', None) == 'True'):\n            session['401'] = 'Token Expired'\n            abort(401)\n        logstr = \"Docklet Response: user = %s result = %s, url = %s\" % (session['username'], result, url)\n        if (sys.getsizeof(logstr) > 512):\n            logstr = \"Docklet Response: user = %s, url = %s\"%(session['username'], url)\n        logger.info(logstr)\n        return result\n        #except:\n            #abort(500)\n\n    @classmethod\n    def getdesc(self,mastername):\n        return env.getenv(mastername+\"_desc\")[1:-1]\n\n    @classmethod\n    def getalldesc(self):\n        masterips = self.post_to_all()\n        res={}\n        for masterip in masterips:\n            mastername = getname(masterip)\n            res[mastername]=env.getenv(mastername+\"_desc\")\n        return res\n\n    @classmethod\n    def post_to_all(self, url = '/', data={}):\n        if (url == '/'):\n            res = []\n            for masterip in masterips:\n                try:\n                    requests.post(\"http://\"+getip(masterip)+\":\"+master_port+\"/isalive/\",data=data)\n                except Exception as e:\n                    logger.debug(e)\n                    continue\n                res.append(masterip)\n            return res\n        data = dict(data)\n        data['token'] = session['token']\n        logger.info(\"Docklet Request: user = %s data = %s, url = %s\"%(session['username'], data, url))\n        result = {}\n        for masterip in masterips:\n            try:\n                res = requests.post(\"http://\"+getip(masterip)+\":\"+master_port+url,data=data).json()\n            except Exception as e:\n                logger.debug(traceback.format_exc())\n                continue\n            if 'success' in res and res['success'] == 'true':\n                result[masterip] = res\n                logger.info(\"get result from %s success\" % getip(masterip))\n            else:\n                logger.error(\"get result from %s failed\" % getip(masterip))\n\n        return result\n\n    @classmethod\n    def unauthorizedpost(self, url = '/', data = None):\n        data = dict(data)\n        data_log = {'user': data.get('user', 'external')}\n        logger.info(\"Docklet Unauthorized Request: data = %s, url = %s\" % (data_log, url))\n        result = requests.post(user_endpoint + url, data = data).json()\n        logger.info(\"Docklet Unauthorized Response: result = %s, url = %s\"%(result, url))\n        return result\n"
  },
  {
    "path": "web/webViews/log.py",
    "content": "#!/usr/bin/env python\n\nimport logging\nimport logging.handlers\nimport argparse\nimport sys\nimport time  # this is only being used as part of the example\nimport os\n\nimport os, sys, inspect\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))\nsrc_folder = os.path.realpath(os.path.abspath(os.path.join(this_folder,\"../..\", \"src\")))\nif src_folder not in sys.path:\n    sys.path.insert(0, src_folder)\nfrom utils import env\n\n# logger should only be imported after initlogging has been called\nlogger = None\n\ndef initlogging(name='docklet'):\n    # Deafults\n    global logger\n\n    homepath = env.getenv('FS_PREFIX')\n    LOG_FILENAME = homepath + '/local/log/' + name + '.log'\n\n    LOG_LEVEL = env.getenv('WEB_LOG_LEVEL')\n    if LOG_LEVEL == \"DEBUG\":\n        LOG_LEVEL = logging.DEBUG\n    elif LOG_LEVEL == \"INFO\":\n        LOG_LEVEL = logging.INFO\n    elif LOG_LEVEL == \"WARNING\":\n        LOG_LEVEL = logging.WARNING\n    elif LOG_LEVEL == \"ERROR\":\n        LOG_LEVEL = logging.ERROR\n    elif LOG_LEVEL == \"CRITICAL\":\n        LOG_LEVEL = logging.CRITIAL\n    else:\n        LOG_LEVEL = logging.DEBUG\n\n    logger = logging.getLogger(name)\n    # Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data\n    # Give the logger a unique name (good practice)\n    # Set the log level to LOG_LEVEL\n    logger.setLevel(LOG_LEVEL)\n    # Make a handler that writes to a file, making a new file at midnight and keeping 3 backups\n    handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME,\n            when=\"midnight\", backupCount=0, encoding='utf-8')\n    # Format each log message like this\n    formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(module)s[%(lineno)d] %(message)s')\n    # Attach the formatter to the handler\n    handler.setFormatter(formatter)\n    # Attach the handler to the logger\n    logger.addHandler(handler)\n\n    # Replace stdout with logging to file at INFO level\n    sys.stdout = RedirectLogger(logger, logging.INFO)\n    # Replace stderr with logging to file at ERROR level\n    sys.stderr = RedirectLogger(logger, logging.ERROR)\n\n    # Make a class we can use to capture stdout and sterr in the log\nclass RedirectLogger(object):\n    def __init__(self, logger, level):\n        \"\"\"Needs a logger and a logger level.\"\"\"\n        self.logger = logger\n        self.level = level\n\n    def write(self, message):\n        # Only log if there is a message (not just a new line)\n        if message.rstrip() != \"\":\n            self.logger.log(self.level, message.rstrip())\n\n    def flush(self):\n        for handler in self.logger.handlers:\n            handler.flush()\n"
  },
  {
    "path": "web/webViews/monitor.py",
    "content": "from flask import session\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\n\nclass statusView(normalView):\n    template_path = \"monitor/status.html\"\n\n    @classmethod\n    def get(self):\n        data = {}\n        allclusters = dockletRequest.post_to_all('/cluster/list/')\n        for master in allclusters:\n            allclusters[master] = allclusters[master].get('clusters')\n        result = dockletRequest.post('/user/selfQuery/')\n        quotas = result['data']['groupinfo']\n        quotanames = quotas.keys()\n        '''result = dockletRequest.post('/monitor/user/quotainfo/', data)\n        quotainfo = result.get('quotainfo')\n        quotainfo['cpu'] = int(int(quotainfo['cpu']))\n        print(quotainfo)'''\n        allcontainers = {}\n        if (result):\n            for master in allclusters:\n                allcontainers[master] = {}\n                for cluster in allclusters[master]:\n                    data[\"clustername\"] = cluster\n                    message = dockletRequest.post('/cluster/info/', data, master.split(\"@\")[0])\n                    if (message):\n                        message = message.get('message')\n                    else:\n                        self.error()\n                    allcontainers[master][cluster] = message\n                message = dockletRequest.post('/batch/vnodes/list/', data, master.split(\"@\")[0])\n                message = message.get('data')\n                containers = []\n                for m in message:\n                    container = {}\n                    container['containername'] = m\n                    container['ip'] = '--'\n                    containers.append(container)\n                tmp = {}\n                tmp['containers'] = containers\n                tmp['status'] = 'running'\n                allcontainers[master]['Batch_Job'] = tmp\n            return self.render(self.template_path,  quotas = quotas, quotanames = quotanames, allcontainers = allcontainers, user = session['username'])\n        else:\n            self.error()\n\nclass statusRealtimeView(normalView):\n    template_path = \"monitor/statusRealtime.html\"\n    node_name = \"\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"user\": session['username'],\n        }\n        result = dockletRequest.post('/monitor/vnodes/%s/basic_info/'%(self.node_name), data, masterip)\n        basic_info = result.get('monitor').get('basic_info')\n        return self.render(self.template_path, node_name = self.node_name, user = session['username'], container = basic_info, masterip=masterip)\n\nclass historyView(normalView):\n    template_path = \"monitor/history.html\"\n\n    @classmethod\n    def get(self):\n        data = {\n            \"user\": session['username'],\n        }\n        allvnodes = {}\n        result = dockletRequest.post_to_all('/monitor/user/createdvnodes/', data)\n        for master in result:\n            allvnodes[master] = result[master].get('createdvnodes')\n        return self.render(self.template_path, user = session['username'],allvnodes = allvnodes)\n\nclass historyVNodeView(normalView):\n    template_path = \"monitor/historyVNode.html\"\n    vnode_name = \"\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"user\": session['username'],\n        }\n        result = dockletRequest.post('/monitor/vnodes/%s/history/'%(self.vnode_name), data, masterip)\n        history = result.get('monitor').get('history')\n        return self.render(self.template_path, vnode_name = self.vnode_name, user = session['username'], history = history)\n\nclass hostsRealtimeView(normalView):\n    template_path = \"monitor/hostsRealtime.html\"\n    com_ip = \"\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"user\": session['username'],\n        }\n        result = dockletRequest.post('/monitor/hosts/%s/cpuconfig/'%(self.com_ip), data,masterip)\n        proc = result.get('monitor').get('cpuconfig')\n        result = dockletRequest.post('/monitor/hosts/%s/osinfo/'%(self.com_ip), data,masterip)\n        osinfo = result.get('monitor').get('osinfo')\n        result = dockletRequest.post('/monitor/hosts/%s/diskinfo/'%(self.com_ip), data,masterip)\n        diskinfos = result.get('monitor').get('diskinfo')\n\n        return self.render(self.template_path, com_ip = self.com_ip, user = session['username'],processors = proc, OSinfo = osinfo, diskinfos = diskinfos, masterip = masterip)\n\nclass hostsConAllView(normalView):\n    template_path = \"monitor/hostsConAll.html\"\n    com_ip = \"\"\n\n    @classmethod\n    def get(self):\n        masterip = self.masterip\n        data = {\n            \"user\": session['username'],\n        }\n        result = dockletRequest.post('/monitor/hosts/%s/containerslist/'%(self.com_ip), data, masterip)\n        containers = result.get('monitor').get('containerslist')\n        containerslist = []\n        for container in containers:\n            result = dockletRequest.post('/monitor/vnodes/%s/basic_info/'%(container), data, masterip)\n            basic_info = result.get('monitor').get('basic_info')\n            result = dockletRequest.post('/monitor/vnodes/%s/owner/'%(container), data, masterip)\n            owner = result.get('monitor')\n            basic_info['owner'] = owner\n            containerslist.append(basic_info)\n        return self.render(self.template_path, containerslist = containerslist, com_ip = self.com_ip, user = session['username'], masterip = masterip)\n\nclass hostsView(normalView):\n    template_path = \"monitor/hosts.html\"\n\n    @classmethod\n    def get(self):\n        data = {\n            \"user\": session['username'],\n        }\n        allresult = dockletRequest.post_to_all('/monitor/listphynodes/', data)\n        allmachines = {}\n        for master in allresult:\n            allmachines[master] = []\n            iplist = allresult[master].get('monitor').get('allnodes')\n            for ip in iplist:\n                containers = {}\n                result = dockletRequest.post('/monitor/hosts/%s/containers/'%(ip), data, master.split(\"@\")[0])\n                containers = result.get('monitor').get('containers')\n                result = dockletRequest.post('/monitor/hosts/%s/status/'%(ip), data, master.split(\"@\")[0])\n                status = result.get('monitor').get('status')\n                allmachines[master].append({'ip':ip,'containers':containers, 'status':status})\n        #print(machines)\n        return self.render(self.template_path, allmachines = allmachines, user = session['username'])\n\nclass monitorUserAllView(normalView):\n    template_path = \"monitor/monitorUserAll.html\"\n\n    @classmethod\n    def get(self):\n        data = {\n            \"user\": session['username'],\n        }\n        result = dockletRequest.post('/monitor/listphynodes/', data)\n        userslist = [{'name':'root'},{'name':'libao'}]\n        for user in userslist:\n            result = dockletRequest.post('/monitor/user/%s/clustercnt/'%(user['name']), data)\n            user['clustercnt'] = result.get('monitor').get('clustercnt')\n        return self.render(self.template_path, userslist = userslist, user = session['username'])\n"
  },
  {
    "path": "web/webViews/notification/notification.py",
    "content": "import json\n\nfrom flask import session, render_template, redirect, request\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\n\nclass NotificationView(normalView):\n    template_path = 'notification.html'\n\n    @classmethod\n    def get(cls):\n        result = dockletRequest.post('/notification/list/')\n        groups = dockletRequest.post('/user/groupNameList/')['groups']\n        notifications = result['data']\n        return cls.render(cls.template_path, notifications=notifications, groups=groups)\n\n\nclass CreateNotificationView(normalView):\n    template_path = 'create_notification.html'\n\n    @classmethod\n    def get(cls):\n        groups = dockletRequest.post('/user/groupNameList/')['groups']\n        return cls.render(cls.template_path, groups=groups)\n\n    @classmethod\n    def post(cls):\n        dockletRequest.post('/notification/create/', request.form)\n        # return redirect('/admin/')\n        return redirect('/notification/')\n\n\nclass QuerySelfNotificationsView(normalView):\n    @classmethod\n    def post(cls):\n        result = dockletRequest.post('/notification/query_self/')\n        return json.dumps(result)\n\n\nclass QueryNotificationView(normalView):\n    template_path = 'notification_info.html'\n\n    @classmethod\n    def get_by_id(cls, notify_id):\n        notifies = []\n        if notify_id == 'all':\n            notifies.extend(dockletRequest.post('/notification/query/all/')['data'])\n        else:\n            notifies.append(dockletRequest.post('/notification/query/', data={'notify_id': notify_id})['data'])\n        return cls.render(cls.template_path, notifies=notifies)\n\n\nclass ModifyNotificationView(normalView):\n    @classmethod\n    def post(cls):\n        dockletRequest.post('/notification/modify/', request.form)\n        return redirect('/notification/')\n\n\nclass DeleteNotificationView(normalView):\n    @classmethod\n    def post(cls):\n        dockletRequest.post('/notification/delete/', request.form)\n        return redirect('/notification/')\n"
  },
  {
    "path": "web/webViews/reportbug.py",
    "content": "from flask import session,render_template,request,redirect\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\n\nclass reportBugView(normalView):\n    template_path = \"opsuccess.html\"\n\n    @classmethod\n    def get(self):\n        dockletRequest.post(\"/bug/report/\", {'bugmessage': self.bugmessage})\n        return self.render(self.template_path, message=\"Thank You!\")\n\n    @classmethod\n    def post(self):\n        return self.get()\n"
  },
  {
    "path": "web/webViews/syslogs.py",
    "content": "from flask import session,render_template,redirect, request\nfrom webViews.view import normalView\nfrom webViews.dockletrequest import dockletRequest\n\nclass logsView(normalView):\n    template_path = \"logs.html\"\n\n    @classmethod\n    def get(self):\n        logs = dockletRequest.post('/logs/list/')['result']\n        logs.sort()\n        logs.sort(key = len)\n        return self.render(self.template_path, logs = logs)\n"
  },
  {
    "path": "web/webViews/user/grouplist.py",
    "content": "from flask import redirect, request\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.view import normalView\nimport json\n\nclass grouplistView(normalView):\n    template_path = \"user/grouplist.html\"\n\nclass groupdetailView(normalView):\n    @classmethod\n    def post(self):\n        return json.dumps(dockletRequest.post('/user/groupList/'))\n\nclass groupqueryView(normalView):\n    @classmethod\n    def post(self):\n        return json.dumps(dockletRequest.post('/user/groupQuery/', request.form))\n\nclass groupmodifyView(normalView):\n    @classmethod\n    def post(self):\n        result =  json.dumps(dockletRequest.post('/user/groupModify/', request.form))\n        return redirect('/settings/')\n"
  },
  {
    "path": "web/webViews/user/userActivate.py",
    "content": "from flask import render_template, redirect, request\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.view import normalView\n\n\nclass userActivateView(normalView):\n    template_path = 'user/activate.html'\n\n    @classmethod\n    def get(self):\n        userinfo = dockletRequest.post('/user/selfQuery/')\n        userinfo = userinfo[\"data\"]\n        if (userinfo[\"description\"] == ''):\n            userinfo[\"description\"] = \"Describe why you want to use Docklet\"\n        return self.render(self.template_path, info = userinfo)\n\n    @classmethod\n    def post(self):\n        dockletRequest.post('/register/', request.form)\n        return redirect('/logout/')\n"
  },
  {
    "path": "web/webViews/user/userinfo.py",
    "content": "from flask import redirect, request\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.authenticate import login\nfrom webViews.view import normalView\nimport json\n\nclass userinfoView(normalView):\n    template_path = \"user/info.html\"\n\n    @classmethod\n    def get(self):\n        userinfo = dockletRequest.post('/user/selfQuery/')\n        userinfo = userinfo[\"data\"]\n        return self.render(self.template_path, info = userinfo)\n\n    @classmethod\n    def post(self):\n        result =  json.dumps(dockletRequest.post('/user/selfModify/', request.form))\n        login.refreshInfo()\n        return result\n"
  },
  {
    "path": "web/webViews/user/userlist.py",
    "content": "from flask import render_template, redirect, request\nfrom webViews.dockletrequest import dockletRequest\nfrom webViews.view import normalView\nimport json\n\nclass userlistView(normalView):\n    template_path = \"user_list.html\"\n\n    @classmethod\n    def get(self):\n        groups = dockletRequest.post('/user/groupNameList/')[\"groups\"]\n        applications = dockletRequest.post('/beans/admin/applymsgs/').get(\"applymsgs\")\n        return self.render(self.template_path, groups = groups, applications = applications)\n\n    @classmethod\n    def post(self):\n        return json.dumps(dockletRequest.post('/user/data/'))\n\n\nclass useraddView(normalView):\n    @classmethod\n    def post(self):\n        dockletRequest.post('/user/add/', request.form)\n        return redirect('/user/list/')\n\nclass userdataView(normalView):\n    @classmethod\n    def get(self):\n        return json.dumps(dockletRequest.post('/user/data/', request.form))\n\n    @classmethod\n    def post(self):\n        return json.dumps(dockletRequest.post('/user/data/', request.form))\n\nclass userqueryView(normalView):\n    @classmethod\n    def get(self):\n        return json.dumps(dockletRequest.post('/user/query/', request.form))\n\n    @classmethod\n    def post(self):\n        return json.dumps(dockletRequest.post('/user/query/', request.form))\n\nclass usermodifyView(normalView):\n    @classmethod\n    def post(self):\n        try:\n            dockletRequest.post('/user/modify/', request.form)\n        except:\n            return self.render('user/mailservererror.html')\n        return redirect('/user/list/')\n"
  },
  {
    "path": "web/webViews/view.py",
    "content": "from flask import render_template, request, abort, session\nfrom webViews.dockletrequest import dockletRequest\n\nimport os, inspect\nthis_folder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))\n\nversion_file = open(this_folder + '/../../VERSION')\nversion = version_file.read()\nversion_file.close()\n\nclass normalView():\n    template_path = \"dashboard.html\"\n\n    @classmethod\n    def get(self):\n        return self.render(self.template_path)\n\n    @classmethod\n    def post(self):\n        return self.render(self.template_path)\n\n    @classmethod\n    def error(self):\n        abort(404)\n\n    @classmethod\n    def as_view(self):\n        if request.method == 'GET':\n            return self.get()\n        elif request.method == 'POST':\n            return self.post()\n        else:\n            return self.error()\n\n    @classmethod\n    def render(self, *args, **kwargs):\n        self.mysession = dict(session)\n        kwargs['mysession'] = self.mysession\n        kwargs['version'] = version\n        result = dockletRequest.post(\"/user/selfQuery/\",{})\n        kwargs['beans'] = result.get(\"data\").get(\"beans\")\n        return render_template(*args, **kwargs)\n"
  }
]