├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── api.rst ├── bin └── lwp ├── debian ├── compat ├── control ├── install ├── lwp.postinst ├── lwp.service ├── lwp.upstart └── rules ├── fabfile.py ├── jsbuild ├── Gruntfile.js ├── README.md ├── bower.json └── package.json ├── lwp.db ├── lwp.db.base ├── lwp.example.conf ├── lwp ├── __init__.py ├── app.py ├── authenticators │ ├── __init__.py │ ├── database.py │ ├── htpasswd.py │ ├── http.py │ ├── ldap.py │ ├── pam.py │ └── stub.py ├── exceptions.py ├── lxclite │ ├── LICENSE │ └── __init__.py ├── static │ ├── css │ │ └── site.css │ └── ico │ │ └── favicon.ico ├── templates │ ├── about.html │ ├── checkconfig.html │ ├── edit.html │ ├── includes │ │ ├── aside.html │ │ ├── modal_backup.html │ │ ├── modal_clone.html │ │ ├── modal_create.html │ │ ├── modal_delete.html │ │ ├── modal_destroy.html │ │ ├── modal_new_user.html │ │ ├── modal_reboot.html │ │ └── nav.html │ ├── index.html │ ├── layout.html │ ├── login.html │ ├── lxc-net.html │ ├── tokens.html │ └── users.html ├── utils.py ├── version └── views │ ├── __init__.py │ ├── api.py │ ├── auth.py │ └── main.py ├── requirements-dev.txt ├── setup.cfg ├── setup.py └── tests ├── api.py ├── auth.py ├── browser.py ├── lxc_lite.py ├── mock-lxc ├── lxc-clone ├── lxc-config ├── lxc-create ├── lxc-destroy ├── lxc-freeze ├── lxc-ls ├── lxc-start ├── lxc-stop └── lxc-unfreeze ├── mock_lxc.py └── utils.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | indent_style = space 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled python modules 2 | *.pyc 3 | 4 | # Local database 5 | lwp.db 6 | lwp.conf 7 | 8 | # Debian packaging 9 | debian/*debhelper* 10 | debian/*substvars 11 | debian/files 12 | debian/lwp 13 | debian/changelog 14 | 15 | # Github pages 16 | gh-pages 17 | 18 | # Python packaging 19 | *.egg-info 20 | dist 21 | build 22 | 23 | # Pycharm conf 24 | .idea/ 25 | 26 | # bower 27 | jsbuild/node_modules/* 28 | jsbuild/bower_components/* 29 | lwp/static/css/*.css 30 | lwp/static/js/*.js 31 | lwp/static/img/* 32 | 33 | #coverage 34 | htmlcov/ 35 | .coverage 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | install: 4 | - pip install -r requirements-dev.txt 5 | - sudo add-apt-repository -y ppa:chris-lea/node.js 6 | - sudo apt-get update -qq 7 | - sudo apt-get install nodejs build-essential dpkg-dev 8 | 9 | before_script: 10 | - sudo mkdir -m 777 /etc/lwp 11 | - sudo mkdir -m 777 -p /var/lwp 12 | - sudo cp lwp.db /var/lwp 13 | - export PATH=$PATH:$TRAVIS_BUILD_DIR/tests/mock-lxc 14 | # mock the presence of lxc-templates 15 | - sudo mkdir -p /usr/share/lxc/templates 16 | - sudo touch /usr/share/lxc/templates/lxc-busybox /usr/share/lxc/templates/lxc-ubuntu 17 | 18 | script: 19 | - fab build_assets 20 | - ./setup.py develop 21 | # test the genearation of secret file without lwp.conf 22 | - nosetests --cover-package=lwp --with-coverage tests/utils.py 23 | - cp lwp.example.conf /etc/lwp/lwp.conf 24 | - fab dev_test # dev_test run all the other tests and flake8 25 | 26 | after_success: 27 | coveralls 28 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | Version 0.9 (31-01-2016) 2 | =========================================================== 3 | 4 | Added Features 5 | ----------------------- 6 | 7 | * Display boot flag in main view 8 | * Add auth method 9 | * Improving test 10 | * Added systemd support 11 | * Add loop backing storage option 12 | 13 | Bug Fixes 14 | ----------------------- 15 | 16 | * Rename generate session method 17 | * Fix regexs used in edit form 18 | * Fix regex to allow creation of container of appropriate size over LVM backend 19 | 20 | Thanks to Lajos Santa, Lukáš Raška, Marcos Silva Cunha, Marton Suranyi, Sebastian Reimers and Yoav Steinberg for their contributions to this release. 21 | 22 | 23 | Other 24 | ------------------------ 25 | 26 | Since this release you are encuraged to use the packagecloud.io repo instead of github repo. 27 | 28 | 29 | Version 0.8 (12-02-2015) 30 | =========================================================== 31 | 32 | TODO 33 | 34 | Thanks to Alexander Knöbel, Boscorillium, Claudio Cesar Sanchez Tejeda, 35 | Claudio Valenti and Lukáš Raška for their contributions to this release. 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2014 Claudio Mignanti and lwp community 4 | 5 | Original authors: 6 | Copyright (c) 2013 Antoine TANZILLI, Élie DELOUMEAU 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.rst 2 | recursive-include lwp LICENSE version 3 | recursive-include lwp/static * 4 | recursive-include lwp/templates * -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | LXC-Web-Panel 2 | ============= 3 | 4 | .. image:: https://travis-ci.org/claudyus/LXC-Web-Panel.svg?branch=master 5 | :target: https://travis-ci.org/claudyus/LXC-Web-Panel 6 | 7 | This is a fork of the original LXC-Web-Panel from https://github.com/lxc-webpanel/LXC-Web-Panel with a lot of improvements and bug fixes for LXC 1.0+. 8 | 9 | This version of lwp features backup capability, RestAPI interface, LDAP support, and other necessary fixes to work with the latest lxc version. This project is also available as debian package for easy installation. 10 | 11 | If you use this fork please ensure to use at least lxc 1.0.4. The code was tested on Ubuntu 12.04 and 14.04. 12 | 13 | On ubuntu 12.04 you should install: 14 | 15 | - LXC from this ppa: https://launchpad.net/~ubuntu-lxc/+archive/daily 16 | - python-flask from ppa: https://launchpad.net/~chris-lea/+archive/python-flask 17 | 18 | Installation on deb based system 19 | ------------------------------------ 20 | 21 | The latest debian packages are released using packagecloud.io service since version 0.9, please update your repo config. 22 | The install script can be found at https://packagecloud.io/claudyus/LXC-Web-Panel/install 23 | 24 | Installation of the package can be done typing:: 25 | 26 | sudo apt-get install lwp 27 | 28 | Version released before 0.9 can be downloaded at http://claudyus.github.io/LXC-Web-Panel/download.html 29 | 30 | 31 | Installation on rpm system or from source code 32 | ---------------------------------------------- 33 | 34 | If you want to run lwp from source code or in a rpm based system like Fedora you can follow the steps below. 35 | 36 | On a fedora system you should install these deps. 37 | 38 | :: 39 | 40 | sudo yum update 41 | sudo yum install lxc lxc-devel lxc-libs lxc-extra lxc-templates python-pam python-flask fabric pytz npm 42 | 43 | Now you should download the source code and inside the source code directory run these steps below 44 | 45 | :: 46 | 47 | fab build_assets # build assets using python-fabric 48 | ./setup.py develop # install python package 49 | mkdir -p /etc/lwp # create config/var dirs and popolate it 50 | mkdir -p /var/lwp 51 | cp lwp.example.conf /etc/lwp/lwp.conf 52 | cp lwp.db /var/lwp/lwp.db 53 | service firewalld stop # for fedora 54 | service lxc start # if service lxc exists 55 | ./bin/lwp --debug # run lwp wth debug support 56 | 57 | 58 | Configuration 59 | ------------- 60 | 61 | 1. Copy /etc/lwp/lwp.example.conf to /etc/lwp/lwp.conf 62 | 2. edit it 63 | 3. start lwp service as root ``service lwp start`` 64 | 65 | Your lwp panel is now at http://localhost:5000/ and default username and password are admin/admin. 66 | 67 | SSL configuration 68 | ^^^^^^^^^^^^^^^^^ 69 | 70 | SSL direct support was dropped after v0.6 release. 71 | 72 | You can configure nginx as reverse proxy if you want to use SSL encryption, see `bug #34 `_ for info. 73 | 74 | 75 | Authentication methods 76 | ^^^^^^^^^^^^^^^^^^^^^^ 77 | 78 | Default authentication is against the internal sqlite database, but it's possible to configure alternative backends. 79 | 80 | LDAP 81 | ++++ 82 | 83 | To enable ldap auth you should set ``auth`` type to ``ldap`` inside your config file then configure all options inside ldap section. 84 | See lwp.example.conf for references. 85 | 86 | Pyhton LDAP need to be installed:: 87 | 88 | apt-get install python-ldap 89 | 90 | htpasswd 91 | ++++++++ 92 | 93 | To enable authentication against htpasswd file you should set ``auth`` type to ``htpasswd`` and ``file`` variable in ``htpasswd`` section to point to the htpasswd file. 94 | 95 | This backend use the crypt function, here an example where ``-d`` force the use of crypt encryption when generating the htpasswd file:: 96 | 97 | htpasswd -d -b -c /etc/lwp/httpasswd admin admin 98 | 99 | PAM 100 | +++ 101 | 102 | To enable authentication against PAM you should set ``auth`` type to ``pam`` and ``service`` variable in ``pam`` section. 103 | Python PAM module needs to be installed:: 104 | 105 | apt-get install python-pam 106 | 107 | or 108 | 109 | :: 110 | 111 | pip install pam 112 | 113 | or 114 | 115 | :: 116 | 117 | yum install python-pam 118 | 119 | With default ``login`` service all valid linux users can login to lwp. 120 | Many more options are available via PAM Configuration, see PAM docs. 121 | 122 | HTTP 123 | +++++ 124 | 125 | This auth method is used to authenticate the users using an external http server through a POST request. To enable this method ``auth`` type to ``http`` and configure the option under ``http`` section. 126 | 127 | Custom autenticators 128 | ++++++++++++++++++++ 129 | 130 | If you want to use different type of authentication, create appropriate file in ``authenticators/`` directory with specific structure (example can be viewed in ``stub`` authenticator) 131 | 132 | File-bucket configuration 133 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 134 | 135 | To enable `file bucket `_ integration for the backup routine you shoul set to ``true`` the ``buckets`` key inside the global section of configuation file. 136 | Than add a section ``buckets`` like this:: 137 | 138 | [global] 139 | . 140 | . 141 | buckets = True 142 | 143 | [buckets] 144 | buckets_host = remote_lan_ip 145 | buckets_port = 1234 146 | 147 | 148 | Developers/Debug 149 | ---------------- 150 | 151 | After a fresh git clone you should download the bower component and setup the package for development purpose. 152 | 153 | :: 154 | 155 | fab build_assets 156 | sudo ./setup.py develop 157 | cp lwp.example.conf lwp.conf 158 | 159 | Now you can run lwp locally using ``sudo ./bin/lwp --debug`` 160 | 161 | Debug is just one of the available options to profile lwp you can use ``--profiling`` options, those options can also be 162 | used against the global installation using: ``sudo lwp --debug`` 163 | 164 | Anyway ensure to stop the lwp service if any: ``sudo service lwp stop`` 165 | 166 | To run test locally unsure that mock-lxc scripts are in PATH (``export PATH=`pwd`/tests/mock-lxc:$PATH``) than run ``fab dev_test`` 167 | 168 | To build a local debian package run ``fab debian`` 169 | 170 | LICENSE 171 | ------- 172 | This work is released under MIT License, see LICENSE file. 173 | -------------------------------------------------------------------------------- /api.rst: -------------------------------------------------------------------------------- 1 | 2 | LXC-Web-Panel RestAPI 3 | --------------------- 4 | 5 | Since version 0.6 LXC-Web-Panel support a set of RestAPI described in this documentation. 6 | 7 | All APIs requires authentication. You need to pass a ``private_token`` parameter by url or header. If passed as header, the header name must be "private-token". 8 | 9 | For example: 10 | :: 11 | 12 | curl http://host:5000/api/v1/containers?private_token=d41d8cd98f00b204e9800998ecf8427e 13 | or 14 | curl --header "private-token: d41d8cd98f00b204e9800998ecf8427e" http://host:5000/api/v1/containers 15 | 16 | 17 | API list and description 18 | ^^^^^^^^^^^^^^^^^^^^^^^^ 19 | 20 | :: 21 | 22 | GET /api/v1/containers/ 23 | 24 | Returns all lxc containers on the current machine and brief status information. 25 | 26 | This api will return a json array like: 27 | 28 | :: 29 | 30 | [{"state": "running", "container": "base"}, {"state": "stopped", "container": "test"}] 31 | 32 | ------------ 33 | 34 | :: 35 | 36 | GET /api/v1/containers/ 37 | 38 | Returns full information about the ``name`` container. 39 | 40 | This api will return a json object like: 41 | 42 | :: 43 | 44 | { 45 | "blkio_use": "7.66 MiB", 46 | "cpu_use": "0.67 seconds", 47 | "ip": "192.168.9.100", 48 | "kmem_use": "0 bytes", 49 | "link": "lxcbr0", 50 | "memory_use": "2.02 MiB", 51 | "name": "base", 52 | "pid": "4548", 53 | "rx_bytes": "81.08 KiB", 54 | "state": "RUNNING", 55 | "total_bytes": "81.08 KiB", 56 | "tx_bytes": "0 bytes" 57 | } 58 | 59 | ------------ 60 | 61 | :: 62 | 63 | POST /api/v1/containers/ 64 | 65 | Update a container status. 66 | 67 | This api accept the following parameters in body request: 68 | 69 | - action: the new container status, possible status are start, stop and freeze 70 | 71 | This api returns ``400`` if missed or mispelled json format, ``409`` if container *name* doesn't exist 72 | 73 | ------------ 74 | 75 | :: 76 | 77 | PUT /api/v1/containers/ 78 | 79 | Create or clone a lxc container. 80 | 81 | This api accept the following parameters in body request: 82 | 83 | - name: the name container (mandatory) 84 | - template: the lxc template (mandatory if clone is not present) 85 | - clone: the name of lxc container to clone (mandatory if template is not present) 86 | - store: the appropriate backing store system (optional) 87 | - xargs: optional xargs to be passed to lxc-create 88 | 89 | ------------ 90 | 91 | :: 92 | 93 | DELETE /api/v1/containers/ 94 | 95 | Delete the ``name`` container. 96 | 97 | ------------ 98 | 99 | :: 100 | 101 | POST /api/v1/tokens 102 | 103 | Add a new access token for the api 104 | This api accept the following parameters in body request: 105 | 106 | - token: the new token to add (mandatory) 107 | - description: an optional token description 108 | 109 | ------------ 110 | 111 | :: 112 | 113 | DELETE /api/v1/tokens/ 114 | 115 | Revoke the given ``private-token`` 116 | -------------------------------------------------------------------------------- /bin/lwp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | from lwp import SESSION_SECRET_FILE 7 | 8 | if __name__ == '__main__': 9 | if '--generate-session-secret' in sys.argv[1:]: 10 | key = os.urandom(24) 11 | with os.fdopen(os.open(SESSION_SECRET_FILE, os.O_WRONLY | os.O_CREAT, 0644), 'w') as handle: 12 | handle.write(key) 13 | exit(0) 14 | 15 | from lwp.utils import read_config_file 16 | read_config_file() # init the global config object, init DEBUG 17 | from lwp.app import app, DEBUG 18 | # override debug configuration from command line 19 | app.debug = True if '--debug' in sys.argv[1:] else DEBUG 20 | app.run(host=app.config['ADDRESS'], port=app.config['PORT']) 21 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: lwp 2 | Section: net 3 | Priority: extra 4 | Maintainer: Claudio Mignanti 5 | Build-Depends: debhelper (>=9), python 6 | X-Python-Version: >= 2.7 7 | Vcs-Git: https://github.com/claudyus/LXC-Web-Panel.git 8 | Standards-Version: 3.9.5 9 | 10 | Package: lwp 11 | Architecture: all 12 | Depends: ${misc:Depends}, ${python:Depends}, lxc (>= 1.0), python-flask (>= 0.10) 13 | Description: LXC Web Panel improved 14 | Built ${misc:buildDate} 15 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | lwp.db var/lwp 2 | lwp.example.conf etc/lwp 3 | -------------------------------------------------------------------------------- /debian/lwp.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ -e /etc/lwp/session_secret ] || /usr/bin/lwp --generate-session-secret 4 | 5 | #DEBHELPER# 6 | -------------------------------------------------------------------------------- /debian/lwp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=LXC Web Panel 3 | 4 | [Service] 5 | User=root 6 | Group=root 7 | # Restart=on-failure 8 | # RestartSec=5 9 | Type=simple 10 | ExecStart=/usr/bin/lwp 11 | StandardOutput=null 12 | StandardError=null 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /debian/lwp.upstart: -------------------------------------------------------------------------------- 1 | description "LXC Web Panel" 2 | start on runlevel [2345] 3 | stop on runlevel [06] 4 | 5 | respawn 6 | respawn limit 10 5 7 | 8 | setuid root 9 | setgid root 10 | 11 | exec lwp 12 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | override_dh_gencontrol : 13 | echo "misc:buildDate=$(shell date)" >> debian/lwp.substvars 14 | dh_gencontrol 15 | 16 | %: 17 | dh $@ --with python2 18 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from datetime import datetime 4 | 5 | from fabric.api import local, task, lcd, settings 6 | from pytz.reference import LocalTimezone 7 | 8 | 9 | def generate_package_date(): 10 | """ 11 | Reads the system date and time, including the timezone and returns 12 | a formatted date string suitable for use in the Debian changelog file: 13 | 14 | Example output: Mon, 23 Dec 2013 11:41:00 +1200 15 | """ 16 | today = datetime.now() 17 | localtime = LocalTimezone() 18 | date_with_timezone = today.replace(tzinfo=localtime) 19 | return date_with_timezone.strftime('%a, %d %b %Y %H:%M:%S %z') 20 | 21 | 22 | def generate_debian_changelog(version): 23 | """ 24 | We don't store the debian/changelog file in the GIT repository, but build 25 | it each time from the current GIT tag. 26 | """ 27 | with open('debian/changelog', 'w') as f: 28 | f.write('lwp ({0}) unstable; urgency=low\n\n * LWP release {0}\n\n'.format(version)) 29 | f.write(' -- Claudio Mignanti {}\n'.format(generate_package_date())) 30 | 31 | 32 | def get_version_from_debian_changelog(): 33 | """ 34 | Returns the version of the latest package from the debian changelog. 35 | 36 | Because we build the changelog first anyway (see: create_debian_changelog), 37 | this should return the same version as "git describe --tags". 38 | """ 39 | return local('dpkg-parsechangelog --show-field Version', capture=True) 40 | 41 | 42 | def get_deb_architecture(): 43 | """ 44 | Returns the deb architecture of the local system, e.g. amd64, i386, arm 45 | """ 46 | return local('dpkg --print-architecture', capture=True) 47 | 48 | 49 | @task(alias='deb') 50 | def debian(): 51 | with settings(warn_only=True): 52 | result = local('git diff-index --quiet HEAD --', capture=True) 53 | if result.failed: 54 | print "!!! GIT REPO NOT CLEAN - ABORT PACKAGING !!!" 55 | sys.exit(1) 56 | 57 | version = local('git describe --tag', capture=True) 58 | with open('lwp/version', 'w') as fd: 59 | fd.write('{}\n'.format(version)) 60 | 61 | # the debian changelog is not stored on GIT and rebuilt each time 62 | generate_debian_changelog(version) 63 | local('sudo dpkg-buildpackage -us -uc -b') 64 | 65 | # dpkg-buildpackage places debs one folder above 66 | package = 'lwp_{}_all.deb'.format(version) 67 | 68 | # finally, move package into gh-pages dir 69 | local('sudo mv ../{} gh-pages/debian/'.format(package)) 70 | local('sudo rm ../lwp_*.changes') 71 | 72 | # release package on packagecloud.io 73 | local('package_cloud push claudyus/LXC-Web-Panel/ubuntu/precise gh-pages/debian/{}'.format(package)) 74 | local('package_cloud push claudyus/LXC-Web-Panel/ubuntu/trusty gh-pages/debian/{}'.format(package)) 75 | 76 | @task 77 | def clone(): 78 | if not os.path.exists('gh-pages'): 79 | local('git clone git@github.com:claudyus/LXC-Web-Panel.git gh-pages') 80 | 81 | with lcd('gh-pages'): 82 | local('git checkout origin/gh-pages -b gh-pages || true') 83 | 84 | 85 | @task(alias='assets') 86 | def build_assets(): 87 | """ 88 | Runs the assets pipeline, Grunt, bower, sass, etc. 89 | """ 90 | # only run npm install when needed 91 | if not os.path.exists('jsbuild/node_modules'): 92 | with lcd('jsbuild'): 93 | local('npm install') 94 | 95 | # run Bower, then Grunt 96 | with lcd('jsbuild'): 97 | local('node_modules/.bin/bower install') 98 | local('node_modules/.bin/grunt') 99 | 100 | 101 | @task 102 | def site(): 103 | clone() 104 | build_assets() 105 | debian() 106 | version = local('git describe --tag', capture=True) 107 | local('DEB_PKG=lwp_{}_all.deb make -C gh-pages/'.format(version)) 108 | 109 | @task 110 | def clean_assets(): 111 | local('rm -rf lwp/static/js/vendor') 112 | local('rm -f lwp/static/css/bootstrap.*') 113 | 114 | @task 115 | def clean_jsbuild(): 116 | local('rm -rf jsbuild/node_modules') 117 | local('rm -rf jsbuild/bower_components') 118 | 119 | @task 120 | def clean(): 121 | clean_jsbuild() 122 | clean_assets() 123 | 124 | @task 125 | def dev_test(): 126 | local('flake8 --ignore=E501,E402 lwp/ bin/lwp') 127 | for test_file in ['auth', 'api', 'browser', 'lxc_lite', 'mock_lxc']: 128 | local('nosetests --cover-package=lwp --with-coverage tests/{}.py'.format(test_file)) 129 | local('mv .coverage .coverage.{}'.format(test_file)) 130 | local('coverage combine') 131 | -------------------------------------------------------------------------------- /jsbuild/Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | 8 | concat: { 9 | js: { 10 | src: [ 11 | 'bower_components/jquery/jquery.js', 12 | 'bower_components/bootstrap/bootstrap/js/bootstrap.js', 13 | 'bower_components/bootstrap-switch/build/js/bootstrap-switch.js', 14 | 'bower_components/jqBootstrapValidation/dist/jqBootstrapValidation-1.3.7.js' 15 | ], 16 | dest: '../lwp/static/js/vendor.js' 17 | }, 18 | css: { 19 | src: [ 20 | 'bower_components/bootstrap/bootstrap/css/bootstrap.css', 21 | 'bower_components/bootstrap/bootstrap/css/bootstrap-responsive.css', 22 | 'bower_components/bootstrap-switch/build/css/bootstrap2/bootstrap-switch.css', 23 | 'bower_components/font-awesome/css/font-awesome.css' 24 | ], 25 | dest: '../lwp/static/css/vendor.css' 26 | } 27 | }, 28 | 29 | uglify: { 30 | compress: { 31 | files: { 32 | '../lwp/static/js/vendor.min.js': ['../lwp/static/js/vendor.js'] 33 | }, 34 | options: { 35 | mangle: true 36 | } 37 | } 38 | }, 39 | 40 | cssmin: { 41 | build: { 42 | src: '../lwp/static/css/vendor.css', 43 | dest: '../lwp/static/css/vendor.min.css' 44 | } 45 | }, 46 | 47 | copy: { 48 | main: { 49 | expand: true, 50 | cwd: 'bower_components/bootstrap/', 51 | src: 'img/*', 52 | dest: '../lwp/static/img/', 53 | flatten: true, 54 | filter: 'isFile', 55 | }, 56 | } 57 | }); 58 | 59 | grunt.loadNpmTasks('grunt-contrib-concat'); 60 | grunt.loadNpmTasks('grunt-contrib-copy'); 61 | grunt.loadNpmTasks('grunt-contrib-uglify'); 62 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 63 | 64 | grunt.registerTask('default', ['concat', 'uglify', 'cssmin', 'copy']); 65 | }; 66 | -------------------------------------------------------------------------------- /jsbuild/README.md: -------------------------------------------------------------------------------- 1 | About this directory 2 | ==================== 3 | 4 | In order to not pollute the root directory of this Python project with JS 5 | build tool files, we use a directory "jsbuild" for this. 6 | 7 | This just keeps the project a bit tidier and puts all frontend tools 8 | in one place. 9 | 10 | The npm node_modules and bower_components folders get created here. 11 | 12 | npm, bower, and grunt are all executed from the fabfile.py 13 | 14 | The fabfile.py is then invoked by the debian packaging process. 15 | -------------------------------------------------------------------------------- /jsbuild/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwp-bower", 3 | "version": "0.1", 4 | "authors": [ 5 | "Rob van der Linde ", 6 | "Claudio Mignanti " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ], 16 | "dependencies": { 17 | "jquery": "~2.0.3", 18 | "bootstrap": "https://github.com/alonisser/bower-bootstrap-2.3.2-legacy.git", 19 | "bootstrap-switch": "~2.0.1", 20 | "jqBootstrapValidation": "https://github.com/ReactiveRaven/jqBootstrapValidation.git", 21 | "font-awesome": "~4.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jsbuild/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lwp-jsbuild", 3 | "description": "JavaScript build environment", 4 | "author": "Rob van der Linde", 5 | "tags": [], 6 | "repository": "https://github.com/robvdl/LXC-Web-Panel.git", 7 | "version": "0.1.1", 8 | "engines": { 9 | "node": ">=0.10.11" 10 | }, 11 | "devDependencies": { 12 | "bower": "~1.3.9", 13 | "grunt": "~0.4.5", 14 | "grunt-cli": "~0.1.13", 15 | "grunt-contrib-concat": "~0.5.0", 16 | "grunt-contrib-copy": "~0.7.0", 17 | "grunt-contrib-uglify": "~0.5.1", 18 | "grunt-contrib-cssmin": "~0.10.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lwp.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudyus/LXC-Web-Panel/a5acca4115dfdd738832b57fefe196d92cd6d029/lwp.db -------------------------------------------------------------------------------- /lwp.db.base: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudyus/LXC-Web-Panel/a5acca4115dfdd738832b57fefe196d92cd6d029/lwp.db.base -------------------------------------------------------------------------------- /lwp.example.conf: -------------------------------------------------------------------------------- 1 | # This is the config file for your lwp system, 2 | # you should copy this file in /etc/lwp/lwp.conf 3 | # and modify it 4 | 5 | [global] 6 | address = 0.0.0.0 7 | port = 5000 8 | 9 | # application prefix, empty for none (example usage - /lwp) 10 | prefix = 11 | 12 | # enable debug only in development 13 | # this could lead to unwanted behavior 14 | debug = False 15 | 16 | # if ssl is set to true than define certs here 17 | ssl = False 18 | pkey = mykey.key 19 | cert = mykey.cert 20 | 21 | # use `ldap` or `database` or `htpasswd` or `pam` here as Auth backend 22 | auth = database 23 | 24 | #enable file-bucket features (http://claudyus.github.io/file-bucket/) 25 | buckets = false 26 | 27 | # if auth is database this is the sqlite file that will be used 28 | [database] 29 | file = /var/lwp/lwp.db 30 | 31 | # if auth is htpasswd this is the htpasswd file that will be used 32 | [htpasswd] 33 | file = /var/lwp/htpasswd 34 | 35 | [session] 36 | time = 10 37 | 38 | [storage_repository] 39 | local = /var/lxc-backup 40 | nfs = /mnt/lxc-backup 41 | 42 | # if auth is ldap those config are used 43 | # here `password` is used with the `bind_dn` query if 44 | # you set bind_method = user. 45 | [ldap] 46 | host = ldap_server_ip 47 | port = 389 48 | # if you want to use ldaps (ssl), set ssl = true 49 | ssl = false 50 | # if you want to search the user by using anonymous login 51 | # set bind_method = anon (the parameters bind_dn and 52 | # password will be ignored) 53 | bind_method = user 54 | base = ou=servers,dc=example,dc=com 55 | bind_dn = cn=auth_user,ou=login,dc=example,dc=com 56 | password = auth_user_password 57 | id_mapping = sAMAccountName 58 | display_mapping = displayName 59 | object_class = user 60 | # if you want to restrict the login to all the users of a group 61 | # set required_group = some_group 62 | required_group = 63 | 64 | [buckets] 65 | buckets_host = remote_lan_ip 66 | buckets_port = 1234 67 | 68 | # if auth is pam this is the pam service that will be used 69 | [pam] 70 | service = login 71 | 72 | [http] 73 | auth_url = http://httpbin.org/post 74 | # override the username/password attrib of the post 75 | username = username 76 | password = password 77 | # verify the ssl cert if present. Set to False if self-signed cert is used 78 | ssl_verify = True 79 | -------------------------------------------------------------------------------- /lwp/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function, division 2 | 3 | import os 4 | import re 5 | import time 6 | import platform 7 | import subprocess 8 | import ConfigParser 9 | 10 | from lwp.exceptions import ContainerNotExists, LxcConfigFileNotComplete 11 | from lwp.lxclite import exists, stopped 12 | from lwp.lxclite import lxcdir 13 | from lwp.utils import cgroup_ext 14 | 15 | SESSION_SECRET_FILE = '/etc/lwp/session_secret' 16 | 17 | 18 | class FakeSection(object): 19 | def __init__(self, fp): 20 | self.fp = fp 21 | self.sechead = '[DEFAULT]\n' 22 | 23 | def readline(self): 24 | if self.sechead: 25 | try: 26 | return self.sechead 27 | finally: 28 | self.sechead = None 29 | else: 30 | return self.fp.readline() 31 | 32 | 33 | def del_section(filename=None): 34 | if filename: 35 | with open(filename, 'r') as f: 36 | read = f.readlines() 37 | i = 0 38 | while i < len(read): 39 | if '[DEFAULT]' in read[i]: 40 | del read[i] 41 | break 42 | with open(filename, 'w') as f: 43 | f.writelines(read) 44 | 45 | 46 | def file_exist(filename): 47 | """ 48 | checks if a given file exist or not 49 | """ 50 | try: 51 | with open(filename) as f: 52 | f.close() 53 | return True 54 | except IOError: 55 | return False 56 | 57 | 58 | def memory_usage(name): 59 | """ 60 | returns memory usage in MB 61 | """ 62 | if not exists(name): 63 | raise ContainerNotExists("The container (%s) does not exist!" % name) 64 | if name in stopped(): 65 | return 0 66 | cmd = ['lxc-cgroup -n %s memory.usage_in_bytes' % name] 67 | try: 68 | out = subprocess.check_output(cmd, shell=True).splitlines() 69 | except subprocess.CalledProcessError: 70 | return 0 71 | return int(int(out[0]) / 1024 / 1024) 72 | 73 | 74 | def host_memory_usage(): 75 | """ 76 | returns a dict of host memory usage values 77 | {'percent': int((used/total)*100), 78 | 'percent_cached':int((cached/total)*100), 79 | 'used': int(used/1024), 80 | 'total': int(total/1024)} 81 | """ 82 | total, free, buffers, cached = 0, 0, 0, 0 83 | with open('/proc/meminfo') as out: 84 | for line in out: 85 | parts = line.split() 86 | key = parts[0] 87 | value = parts[1] 88 | if key == 'MemTotal:': 89 | total = float(value) 90 | if key == 'MemFree:': 91 | free = float(value) 92 | if key == 'Buffers:': 93 | buffers = float(value) 94 | if key == 'Cached:': 95 | cached = float(value) 96 | used = (total - (free + buffers + cached)) 97 | return {'percent': int((used / total) * 100), 98 | 'percent_cached': int((cached / total) * 100), 99 | 'used': int(used / 1024), 100 | 'total': int(total / 1024)} 101 | 102 | 103 | def host_cpu_percent(): 104 | """ 105 | returns CPU usage in percent 106 | """ 107 | with open('/proc/stat', 'r') as f: 108 | line = f.readlines()[0] 109 | 110 | data = line.split() 111 | previdle = float(data[4]) 112 | prevtotal = float(data[1]) + float(data[2]) + float(data[3]) + float(data[4]) 113 | time.sleep(0.1) 114 | 115 | with open('/proc/stat', 'r') as f: 116 | line = f.readlines()[0] 117 | 118 | data = line.split() 119 | idle = float(data[4]) 120 | total = float(data[1]) + float(data[2]) + float(data[3]) + float(data[4]) 121 | intervaltotal = total - prevtotal 122 | percent = int(100 * (intervaltotal - (idle - previdle)) / intervaltotal) 123 | return str('%.1f' % percent) 124 | 125 | 126 | def host_disk_usage(partition=None): 127 | """ 128 | returns a dict of disk usage values 129 | {'total': usage[1], 130 | 'used': usage[2], 131 | 'free': usage[3], 132 | 'percent': usage[4]} 133 | """ 134 | partition = lxcdir() 135 | usage = subprocess.check_output(['df -h %s' % partition], shell=True).split('\n')[1].split() 136 | return {'total': usage[1], 137 | 'used': usage[2], 138 | 'free': usage[3], 139 | 'percent': usage[4]} 140 | 141 | 142 | def host_uptime(): 143 | """ 144 | returns a dict of the system uptime 145 | {'day': days, 146 | 'time': '%d:%02d' % (hours,minutes)} 147 | """ 148 | with open('/proc/uptime') as f: 149 | uptime = int(f.readlines()[0].split('.')[0]) 150 | minutes = int(uptime / 60) % 60 151 | hours = int(uptime / 60 / 60) % 24 152 | days = int(uptime / 60 / 60 / 24) 153 | return {'day': days, 154 | 'time': '%d:%02d' % (hours, minutes)} 155 | 156 | 157 | def name_distro(): 158 | """ 159 | return the System version 160 | """ 161 | dist = '%s %s - %s' % platform.linux_distribution() 162 | 163 | return dist 164 | 165 | 166 | def get_templates_list(): 167 | """ 168 | returns a sorted lxc templates list 169 | """ 170 | templates = [] 171 | 172 | try: 173 | path = os.listdir('/usr/share/lxc/templates') 174 | except OSError: 175 | # TODO: if this folder doesn't exist, it will cause a crash 176 | path = os.listdir('/usr/lib/lxc/templates') 177 | 178 | if path: 179 | for line in path: 180 | templates.append(line.replace('lxc-', '')) 181 | 182 | return sorted(templates) 183 | 184 | 185 | def check_version(): 186 | """ 187 | returns latest LWP version (dict with current) 188 | """ 189 | try: 190 | version = subprocess.check_output('git describe --tags', shell=True) 191 | except subprocess.CalledProcessError: 192 | version = open(os.path.join(os.path.dirname(__file__), 'version')).read()[0:-1] 193 | return {'current': version} 194 | 195 | 196 | def get_net_settings(): 197 | """ 198 | returns a dict of all known settings for LXC networking 199 | """ 200 | filename = '/etc/default/lxc-net' 201 | if not file_exist(filename): 202 | filename = '/etc/default/lxc' 203 | if not file_exist(filename): 204 | raise LxcConfigFileNotComplete('Cannot find lxc-net config file! Check if /etc/default/lxc-net exists') 205 | config = ConfigParser.SafeConfigParser() 206 | 207 | config.readfp(FakeSection(open(filename))) 208 | cfg = { 209 | 'use': config.get('DEFAULT', 'USE_LXC_BRIDGE').strip('"'), 210 | 'bridge': config.get('DEFAULT', 'LXC_BRIDGE').strip('"'), 211 | 'address': config.get('DEFAULT', 'LXC_ADDR').strip('"'), 212 | 'netmask': config.get('DEFAULT', 'LXC_NETMASK').strip('"'), 213 | 'network': config.get('DEFAULT', 'LXC_NETWORK').strip('"'), 214 | 'range': config.get('DEFAULT', 'LXC_DHCP_RANGE').strip('"'), 215 | 'max': config.get('DEFAULT', 'LXC_DHCP_MAX').strip('"') 216 | } 217 | 218 | return cfg 219 | 220 | 221 | def get_container_settings(name, status=None): 222 | """ 223 | returns a dict of all utils settings for a container 224 | status is optional and should be set to RUNNING to retrieve ipv4 config (if unset) 225 | """ 226 | filename = '{}/{}/config'.format(lxcdir(), name) 227 | if not file_exist(filename): 228 | return False 229 | config = ConfigParser.SafeConfigParser() 230 | config.readfp(FakeSection(open(filename))) 231 | 232 | cfg = {} 233 | # for each key in cgroup_ext add value to cfg dict and initialize values 234 | for options in cgroup_ext.keys(): 235 | if config.has_option('DEFAULT', cgroup_ext[options][0]): 236 | cfg[options] = config.get('DEFAULT', cgroup_ext[options][0]) 237 | else: 238 | cfg[options] = '' # add the key in dictionary anyway to match form 239 | 240 | # if ipv4 is unset try to determinate it 241 | if cfg['ipv4'] == '' and status == 'RUNNING': 242 | cmd = ['lxc-ls --fancy --fancy-format name,ipv4|grep -w \'%s\\s\' | awk \'{ print $2 }\'' % name] 243 | try: 244 | cfg['ipv4'] = subprocess.check_output(cmd, shell=True) 245 | except subprocess.CalledProcessError: 246 | cfg['ipv4'] = '' 247 | 248 | # parse memlimits to int 249 | cfg['memlimit'] = re.sub(r'[a-zA-Z]', '', cfg['memlimit']) 250 | cfg['swlimit'] = re.sub(r'[a-zA-Z]', '', cfg['swlimit']) 251 | 252 | return cfg 253 | 254 | 255 | def push_net_value(key, value, filename='/etc/default/lxc-net'): 256 | """ 257 | replace a var in the lxc-net config file 258 | """ 259 | if filename: 260 | config = ConfigParser.RawConfigParser() 261 | config.readfp(FakeSection(open(filename))) 262 | if not value: 263 | config.remove_option('DEFAULT', key) 264 | else: 265 | config.set('DEFAULT', key, value) 266 | 267 | with open(filename, 'wb') as configfile: 268 | config.write(configfile) 269 | 270 | del_section(filename=filename) 271 | 272 | with open(filename, 'r') as load: 273 | read = load.readlines() 274 | 275 | i = 0 276 | while i < len(read): 277 | if ' = ' in read[i]: 278 | split = read[i].split(' = ') 279 | split[1] = split[1].strip('\n') 280 | if '\"' in split[1]: 281 | read[i] = '%s=%s\n' % (split[0].upper(), split[1]) 282 | else: 283 | read[i] = '%s=\"%s\"\n' % (split[0].upper(), split[1]) 284 | i += 1 285 | with open(filename, 'w') as load: 286 | load.writelines(read) 287 | 288 | 289 | def push_config_value(key, value, container=None): 290 | """ 291 | replace a var in a container config file 292 | """ 293 | 294 | def save_cgroup_devices(filename=None): 295 | """ 296 | returns multiple values (lxc.cgroup.devices.deny and lxc.cgroup.devices.allow) in a list. 297 | because ConfigParser cannot make this... 298 | """ 299 | if filename: 300 | values = [] 301 | i = 0 302 | 303 | with open(filename, 'r') as load: 304 | read = load.readlines() 305 | 306 | while i < len(read): 307 | if not read[i].startswith('#'): 308 | if not (read[i] in values): 309 | if re.match('lxc.cgroup.devices.deny|lxc.cgroup.devices.allow|lxc.mount.entry|lxc.cap.drop', read[i]): 310 | values.append(read[i]) 311 | i += 1 312 | return values 313 | 314 | if container: 315 | filename = '{}/{}/config'.format(lxcdir(), container) 316 | save = save_cgroup_devices(filename=filename) 317 | 318 | config = ConfigParser.RawConfigParser() 319 | config.readfp(FakeSection(open(filename))) 320 | if not value: 321 | config.remove_option('DEFAULT', key) 322 | elif key == cgroup_ext['memlimit'][0] or key == cgroup_ext['swlimit'][0] and value is not False: 323 | config.set('DEFAULT', key, '%sM' % value) 324 | else: 325 | config.set('DEFAULT', key, value) 326 | 327 | # Bugfix (can't duplicate keys with config parser) 328 | if config.has_option('DEFAULT', cgroup_ext['deny'][0]): 329 | config.remove_option('DEFAULT', cgroup_ext['deny'][0]) 330 | if config.has_option('DEFAULT', cgroup_ext['allow'][0]): 331 | config.remove_option('DEFAULT', cgroup_ext['allow'][0]) 332 | if config.has_option('DEFAULT', 'lxc.cap.drop'): 333 | config.remove_option('DEFAULT', 'lxc.cap.drop') 334 | if config.has_option('DEFAULT', 'lxc.mount.entry'): 335 | config.remove_option('DEFAULT', 'lxc.mount.entry') 336 | 337 | with open(filename, 'wb') as configfile: 338 | config.write(configfile) 339 | 340 | del_section(filename=filename) 341 | 342 | with open(filename, "a") as configfile: 343 | configfile.writelines(save) 344 | 345 | 346 | def net_restart(): 347 | """ 348 | restarts LXC networking 349 | """ 350 | cmd = ['/usr/sbin/service lxc-net restart'] 351 | try: 352 | subprocess.check_call(cmd, shell=True) 353 | return 0 354 | except subprocess.CalledProcessError: 355 | return 1 356 | -------------------------------------------------------------------------------- /lwp/app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import absolute_import, print_function 4 | 5 | import os 6 | import sys 7 | 8 | from flask import Flask, g 9 | 10 | from lwp.utils import connect_db, check_session_limit, config 11 | from lwp import SESSION_SECRET_FILE 12 | from lwp.views import main, auth, api 13 | 14 | try: 15 | SECRET_KEY = open(SESSION_SECRET_FILE, 'r').read() 16 | except IOError: 17 | print(' * Missing session_secret file, your session will not survive server reboot. Run with --generate-session-secret to generate permanent file.') 18 | SECRET_KEY = os.urandom(24) 19 | 20 | DEBUG = config.getboolean('global', 'debug') 21 | DATABASE = config.get('database', 'file') 22 | ADDRESS = config.get('global', 'address') 23 | PORT = int(config.get('global', 'port')) 24 | PREFIX = config.get('global', 'prefix') 25 | 26 | # Flask app 27 | app = Flask('lwp', static_url_path="{0}/static".format(PREFIX)) 28 | app.config.from_object(__name__) 29 | app.register_blueprint(main.mod, url_prefix=PREFIX) 30 | app.register_blueprint(auth.mod, url_prefix=PREFIX) 31 | app.register_blueprint(api.mod, url_prefix=PREFIX) 32 | 33 | 34 | if '--profiling' in sys.argv[1:]: 35 | from werkzeug.contrib.profiler import ProfilerMiddleware 36 | app.config['PROFILE'] = True 37 | app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[30]) 38 | app.debug = True # also enable debug 39 | 40 | 41 | @app.before_request 42 | def before_request(): 43 | """ 44 | executes functions before all requests 45 | """ 46 | check_session_limit() 47 | g.db = connect_db(app.config['DATABASE']) 48 | 49 | 50 | @app.teardown_request 51 | def teardown_request(exception): 52 | """ 53 | executes functions after all requests 54 | """ 55 | if hasattr(g, 'db'): 56 | g.db.close() 57 | -------------------------------------------------------------------------------- /lwp/authenticators/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def get_authenticator(auth): 5 | n = "{}.{}".format(__name__, auth) 6 | module = __import__(n, fromlist=[__name__]) 7 | class_ = getattr(module, auth) 8 | return class_() 9 | -------------------------------------------------------------------------------- /lwp/authenticators/database.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from lwp.utils import query_db, hash_passwd 3 | 4 | 5 | class database: 6 | def authenticate(self, username, password): 7 | hash_password = hash_passwd(password) 8 | return query_db('select name, username, su from users where username=? and password=?', [username, hash_password], one=True) 9 | -------------------------------------------------------------------------------- /lwp/authenticators/htpasswd.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | import hmac 4 | import crypt 5 | 6 | from lwp.utils import read_config_file 7 | 8 | 9 | class htpasswd: 10 | def __init__(self): 11 | self.HTPASSWD_FILE = read_config_file().get('htpasswd', 'file') 12 | 13 | def authenticate(self, username, password): 14 | user = None 15 | if self.check_htpasswd(self.HTPASSWD_FILE, username, password): 16 | user = { 17 | 'username': username, 18 | 'name': username, 19 | 'su': 'Yes' 20 | } 21 | 22 | return user 23 | 24 | def check_htpasswd(self, htpasswd_file, username, password): 25 | htuser = None 26 | 27 | lines = open(htpasswd_file, 'r').readlines() 28 | for line in lines: 29 | htuser, htpasswd = line.split(':') 30 | htpasswd = htpasswd.rstrip('\n') 31 | if username == htuser: 32 | break 33 | 34 | if htuser is None: 35 | return False 36 | else: 37 | if sys.version_info < (2, 7, 7): 38 | return crypt.crypt(password, htpasswd) == htpasswd 39 | else: 40 | return hmac.compare_digest(crypt.crypt(password, htpasswd), htpasswd) 41 | -------------------------------------------------------------------------------- /lwp/authenticators/http.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import requests 3 | 4 | from lwp.utils import read_config_file 5 | 6 | 7 | class http: 8 | 9 | """Check if ``user``/``password`` are valid.""" 10 | 11 | def __init__(self): 12 | self.HTTP_USER = read_config_file().get('http', 'username') 13 | self.HTTP_PASSWORD = read_config_file().get('http', 'password') 14 | self.HTTP_AUTH_URL = read_config_file().get('http', 'auth_url') 15 | self.HTTP_SSL_VERIFY = read_config_file().get('http', 'ssl_verify') 16 | 17 | def authenticate(self, username, password): 18 | payload = {self.HTTP_USER: username, self.HTTP_PASSWORD: password} 19 | return requests.post(self.HTTP_AUTH_URL, data=payload, verify=self.HTTP_SSL_VERIFY).status_code in (200, 201) 20 | -------------------------------------------------------------------------------- /lwp/authenticators/ldap.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from lwp.utils import read_config_file 4 | 5 | import ldap as ldap_m 6 | 7 | 8 | class ldap: 9 | def __init__(self): 10 | config = read_config_file() 11 | self.LDAP_HOST = config.get('ldap', 'host') 12 | self.LDAP_PORT = int(config.get('ldap', 'port')) 13 | self.LDAP_PROTO = 'ldaps' if config.getboolean('ldap', 'ssl') else 'ldap' 14 | self.LDAP_BIND_METHOD = config.get('ldap', 'bind_method') 15 | self.LDAP_BASE = config.get('ldap', 'base') 16 | self.LDAP_BIND_DN = config.get('ldap', 'bind_dn') 17 | self.LDAP_PASS = config.get('ldap', 'password') 18 | self.ID_MAPPING = config.get('ldap', 'id_mapping') 19 | self.DISPLAY_MAPPING = config.get('ldap', 'display_mapping') 20 | self.OBJECT_CLASS = config.get('ldap', 'object_class') 21 | self.REQUIRED_GROUP = config.get('ldap', 'required_group') 22 | 23 | def authenticate(self, username, password): 24 | user = None 25 | try: 26 | l = ldap_m.initialize("{}://{}:{}".format(self.LDAP_PROTO, self.LDAP_HOST, self.LDAP_PORT)) 27 | l.set_option(ldap_m.OPT_REFERRALS, 0) 28 | l.protocol_version = 3 29 | if self.LDAP_BIND_METHOD == 'user': 30 | l.simple_bind(self.LDAP_BIND_DN, self.LDAP_PASS) 31 | else: 32 | l.simple_bind() 33 | attrs = ['memberOf', self.ID_MAPPING, self.DISPLAY_MAPPING] if self.REQUIRED_GROUP else [] 34 | q = l.search_s(self.LDAP_BASE, ldap_m.SCOPE_SUBTREE, "(&(objectClass={})({}={}))".format(self.OBJECT_CLASS, self.ID_MAPPING, username), attrs)[0] 35 | is_member = False 36 | if 'memberOf' in q[1]: 37 | for group in q[1]['memberOf']: 38 | if group.find("cn={},".format(self.REQUIRED_GROUP)) >= 0: 39 | is_member = True 40 | break 41 | if is_member is True or not self.REQUIRED_GROUP: 42 | l.bind_s(q[0], password, ldap_m.AUTH_SIMPLE) 43 | # set the parameters for user by ldap objectClass 44 | user = { 45 | 'username': q[1][self.ID_MAPPING][0].decode('utf8'), 46 | 'name': q[1][self.DISPLAY_MAPPING][0].decode('utf8'), 47 | 'su': 'Yes' 48 | } 49 | except Exception, e: 50 | print(str(e)) 51 | 52 | return user 53 | -------------------------------------------------------------------------------- /lwp/authenticators/pam.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from lwp.utils import config 3 | 4 | # try Debian PAM first (PyPAM) 5 | try: 6 | import PAM 7 | except ImportError: 8 | import pam as pam_m 9 | 10 | 11 | class pam: 12 | def __init__(self): 13 | self.PAM_SERVICE = config.get('pam', 'service') 14 | 15 | def authenticate(self, username, password): 16 | user = None 17 | 18 | # try Debian PAM module (PyPAM) 19 | try: 20 | auth = PAM.pam() 21 | 22 | # pam callback 23 | def pam_conv(auth, query_list, userData): 24 | response = [] 25 | 26 | for i in range(len(query_list)): 27 | query, type = query_list[i] 28 | if type == PAM.PAM_PROMPT_ECHO_ON: 29 | val = raw_input(query) 30 | response.append((val, 0)) 31 | elif type == PAM.PAM_PROMPT_ECHO_OFF: 32 | response.append((password, 0)) 33 | elif type == PAM.PAM_PROMPT_ERROR_MSG or type == PAM.PAM_PROMPT_TEXT_INFO: 34 | response.append(('', 0)) 35 | else: 36 | return None 37 | 38 | return response 39 | 40 | auth.start(self.PAM_SERVICE) 41 | auth.set_item(PAM.PAM_USER, username) 42 | auth.set_item(PAM.PAM_CONV, pam_conv) 43 | try: 44 | auth.authenticate() 45 | auth.acct_mgmt() 46 | 47 | user = { 48 | 'username': username, 49 | 'name': username, 50 | 'su': 'Yes' 51 | } 52 | except PAM.error: 53 | pass 54 | 55 | except NameError: 56 | p = pam_m 57 | if p.authenticate(username, password, service=self.PAM_SERVICE): 58 | user = { 59 | 'username': username, 60 | 'name': username, 61 | 'su': 'Yes' 62 | } 63 | 64 | return user 65 | -------------------------------------------------------------------------------- /lwp/authenticators/stub.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Example authenticator. 3 | # Class has to have the same name as file (case sensitive) and implement authenticate(self, username, password) method. 4 | 5 | 6 | class stub: 7 | def authenticate(self, username, password): 8 | return True 9 | -------------------------------------------------------------------------------- /lwp/exceptions.py: -------------------------------------------------------------------------------- 1 | class LxcConfigFileNotComplete(Exception): 2 | pass 3 | 4 | 5 | class ContainerNotExists(Exception): 6 | pass 7 | 8 | 9 | class ContainerAlreadyExists(Exception): 10 | pass 11 | 12 | 13 | class ContainerDoesntExists(Exception): 14 | pass 15 | 16 | 17 | class ContainerAlreadyRunning(Exception): 18 | pass 19 | 20 | 21 | class ContainerNotRunning(Exception): 22 | pass 23 | 24 | 25 | class DirectoryDoesntExists(Exception): 26 | pass 27 | 28 | 29 | class NFSDirectoryNotMounted(Exception): 30 | pass 31 | -------------------------------------------------------------------------------- /lwp/lxclite/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Élie DELOUMEAU 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lwp/lxclite/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, print_function 2 | 3 | import subprocess 4 | import os 5 | import time 6 | 7 | from lwp.exceptions import ContainerDoesntExists, ContainerAlreadyExists, ContainerAlreadyRunning, ContainerNotRunning,\ 8 | DirectoryDoesntExists, NFSDirectoryNotMounted 9 | 10 | 11 | # LXC Python Library 12 | 13 | # Original author: Elie Deloumeau 14 | # The MIT License (MIT) 15 | 16 | 17 | def _run(cmd, output=False): 18 | """ 19 | To run command easier 20 | """ 21 | if output: 22 | try: 23 | out = subprocess.check_output('{}'.format(cmd), shell=True, close_fds=True) 24 | except subprocess.CalledProcessError: 25 | out = False 26 | return out 27 | return subprocess.check_call('{}'.format(cmd), shell=True, close_fds=True) # returns 0 for True 28 | 29 | 30 | def exists(container): 31 | """ 32 | Check if container exists 33 | """ 34 | if container in ls(): 35 | return True 36 | return False 37 | 38 | 39 | def create(container, template='ubuntu', storage=None, xargs=None): 40 | """ 41 | Create a container (without all options) 42 | Default template: Ubuntu 43 | """ 44 | if exists(container): 45 | raise ContainerAlreadyExists('Container {} already created!'.format(container)) 46 | 47 | command = 'lxc-create -n {}'.format(container) 48 | command += ' -t {}'.format(template) 49 | if storage: 50 | command += ' -B {}'.format(storage) 51 | if xargs: 52 | command += ' -- {}'.format(xargs) 53 | 54 | return _run(command) 55 | 56 | 57 | def clone(orig=None, new=None, snapshot=False): 58 | """ 59 | Clone a container (without all options) 60 | """ 61 | if orig and new: 62 | if exists(new): 63 | raise ContainerAlreadyExists('Container {} already exist!'.format(new)) 64 | 65 | if snapshot: 66 | command = 'lxc-clone -s {} {}'.format(orig, new) 67 | else: 68 | command = 'lxc-clone {} {}'.format(orig, new) 69 | 70 | return _run(command) 71 | 72 | 73 | def info(container): 74 | """ 75 | Check info from lxc-info 76 | """ 77 | if not exists(container): 78 | raise ContainerDoesntExists('Container {} does not exist!'.format(container)) 79 | 80 | output = _run('lxc-info -qn {}'.format(container), output=True).splitlines() 81 | 82 | state = {'pid': 0} 83 | for val in output: 84 | state[val.split(':')[0].lower().strip().replace(" ", "_")] = val.split(':')[1].strip() 85 | 86 | return state 87 | 88 | 89 | def lxcdir(): 90 | return _run('lxc-config lxc.lxcpath', output=True).strip() 91 | 92 | 93 | def ls(): 94 | """ 95 | List containers directory 96 | """ 97 | lxc_dir = lxcdir() 98 | ct_list = [] 99 | 100 | try: 101 | lsdir = os.listdir(lxc_dir) 102 | for i in lsdir: 103 | # ensure that we have a valid path and config file 104 | if os.path.isdir('{}/{}'.format(lxc_dir, i)) and os.path.isfile(('{}/{}/config'.format(lxc_dir, i))): 105 | ct_list.append(i) 106 | except OSError: 107 | ct_list = [] 108 | return sorted(ct_list) 109 | 110 | 111 | def listx(): 112 | """ 113 | List all containers with status (Running, Frozen or Stopped) in a dict 114 | Same as lxc-list or lxc-ls --fancy (0.9) 115 | """ 116 | stopped = [] 117 | frozen = [] 118 | running = [] 119 | status_container = {} 120 | 121 | outcmd = _run('lxc-ls --fancy | grep -o \'^[^-].*\' | tail -n+2', output=True).splitlines() 122 | 123 | for line in outcmd: 124 | status_container[line.split()[0]] = line.split()[1:] 125 | 126 | for container in ls(): 127 | if status_container[container][0] == 'RUNNING': 128 | running.append(container) 129 | elif status_container[container][0] == 'STOPPED': 130 | stopped.append(container) 131 | elif status_container[container][0] == 'FROZEN': 132 | frozen.append(container) 133 | 134 | return {'RUNNING': running, 135 | 'FROZEN': frozen, 136 | 'STOPPED': stopped} 137 | 138 | 139 | def list_status(): 140 | """ 141 | List all containers with status (Running, Frozen or Stopped) in a dict 142 | """ 143 | containers = [] 144 | 145 | for container in ls(): 146 | state = info(container)['state'] 147 | # TODO: figure out why pycharm thinks state is an int 148 | containers.append(dict(container=container, state=state.lower())) 149 | 150 | return containers 151 | 152 | 153 | def running(): 154 | return listx()['RUNNING'] 155 | 156 | 157 | def frozen(): 158 | return listx()['FROZEN'] 159 | 160 | 161 | def stopped(): 162 | return listx()['STOPPED'] 163 | 164 | 165 | def start(container): 166 | """ 167 | Starts a container 168 | """ 169 | if not exists(container): 170 | raise ContainerDoesntExists('Container {} does not exists!'.format(container)) 171 | if container in running(): 172 | raise ContainerAlreadyRunning('Container {} is already running!'.format(container)) 173 | return _run('lxc-start -dn {}'.format(container)) 174 | 175 | 176 | def stop(container): 177 | """ 178 | Stops a container 179 | """ 180 | if not exists(container): 181 | raise ContainerDoesntExists('Container {} does not exists!'.format(container)) 182 | if container in stopped(): 183 | raise ContainerNotRunning('Container {} is not running!'.format(container)) 184 | return _run('lxc-stop -n {}'.format(container)) 185 | 186 | 187 | def freeze(container): 188 | """ 189 | Freezes a container 190 | """ 191 | if not exists(container): 192 | raise ContainerDoesntExists('Container {} does not exists!'.format(container)) 193 | if container not in running(): 194 | raise ContainerNotRunning('Container {} is not running!'.format(container)) 195 | return _run('lxc-freeze -n {}'.format(container)) 196 | 197 | 198 | def unfreeze(container): 199 | """ 200 | Unfreezes a container 201 | """ 202 | if not exists(container): 203 | raise ContainerDoesntExists('Container {} does not exists!'.format(container)) 204 | if container not in frozen(): 205 | raise ContainerNotRunning('Container {} is not frozen!'.format(container)) 206 | return _run('lxc-unfreeze -n {}'.format(container)) 207 | 208 | 209 | def destroy(container): 210 | """ 211 | Destroys a container 212 | """ 213 | if not exists(container): 214 | raise ContainerDoesntExists('Container {} does not exists!'.format(container)) 215 | return _run('lxc-destroy -n {}'.format(container)) 216 | 217 | 218 | def checkconfig(): 219 | """ 220 | Returns the output of lxc-checkconfig (colors cleared) 221 | """ 222 | out = _run('lxc-checkconfig', output=True) 223 | if out: 224 | return out.replace('[1;32m', '').replace('[1;33m', '').replace('[0;39m', '').replace('[1;32m', '').replace('\x1b', '').replace(': ', ':').split('\n') 225 | return out 226 | 227 | 228 | def cgroup(container, key, value): 229 | if not exists(container): 230 | raise ContainerDoesntExists('Container {} does not exist!'.format(container)) 231 | return _run('lxc-cgroup -n {} {} {}'.format(container, key, value)) 232 | 233 | 234 | def backup(container, sr_type='local', destination='/var/lxc-backup/'): 235 | """ 236 | Backup container with tar to a storage repository (SR). E.g: localy or with nfs 237 | If SR is localy then the path is /var/lxc-backup/ 238 | otherwise if SR is NFS type then we just check if the SR is mounted in host side in /mnt/lxc-backup 239 | 240 | Returns path/filename of the backup instances 241 | """ 242 | prefix = time.strftime("%Y-%m-%d__%H-%M.tar.gz") 243 | filename = '{}/{}-{}'.format(destination, container, prefix) 244 | was_running = False 245 | 246 | if not exists(container): 247 | raise ContainerDoesntExists('Container {} does not exist!'.format(container)) 248 | if sr_type == 'local': 249 | if not os.path.isdir(destination): 250 | raise DirectoryDoesntExists('Directory {} does not exist !'.format(destination)) 251 | if sr_type == 'nfs': 252 | if not os.path.ismount(destination): 253 | raise NFSDirectoryNotMounted('NFS {} is not mounted !'.format(destination)) 254 | 255 | if info(container)['state'] == 'RUNNING': 256 | was_running = True 257 | freeze(container) 258 | 259 | _run('tar czf {} -C /var/lib/lxc {}'.format(filename, container)) 260 | 261 | if was_running is True: 262 | unfreeze(container) 263 | 264 | return filename 265 | -------------------------------------------------------------------------------- /lwp/static/css/site.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | } 4 | html { 5 | overflow-y: scroll; 6 | } 7 | h3.head { 8 | margin: 0; 9 | } 10 | h3.head-networking, .switch-networking { 11 | display:inline-block; 12 | } 13 | .switch-networking { 14 | position:relative; 15 | top:-5px; 16 | margin-left:17px; 17 | } 18 | .sidebar-nav { 19 | padding: 9px 0; 20 | } 21 | .table td { 22 | vertical-align: middle; 23 | padding: 0 8px; 24 | text-align: center; 25 | } 26 | .hero-unit { 27 | padding: 5px 30px; 28 | } 29 | .help-block ul { 30 | margin:0 0 -10px 0; 31 | } 32 | .help-block li { 33 | list-style-type:none; 34 | } 35 | .fix-height { 36 | line-height: 30px; 37 | min-height: 30px; 38 | height: 30px; 39 | } 40 | #wrap { 41 | min-height: 100%; 42 | height: auto !important; 43 | height: 100%; 44 | margin: 0 auto -60px; 45 | } 46 | #push, #footer { 47 | height: 60px; 48 | } 49 | #wrap > .container { 50 | padding-top: 60px; 51 | } 52 | .container .credit { 53 | margin: 20px 0; 54 | } 55 | 56 | .contributor { 57 | vertical-align: top; 58 | padding: 0 0 3px 20px; 59 | } 60 | .contributor strong { 61 | position: absolute; 62 | margin: 3px 0 0 10px; 63 | } 64 | .contributor img { 65 | width: 70px; 66 | height: 70px; 67 | } 68 | .contributor commits { 69 | margin-left: 15px; 70 | } 71 | .contributor i { 72 | margin-right: 3px; 73 | } 74 | .buttons span { 75 | margin-left: 7px; 76 | } 77 | -------------------------------------------------------------------------------- /lwp/static/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudyus/LXC-Web-Panel/a5acca4115dfdd738832b57fefe196d92cd6d029/lwp/static/ico/favicon.ico -------------------------------------------------------------------------------- /lwp/templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}About{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 |

LXC Web Panel Version : {{ version.current }}

7 | 8 |
9 |

The MIT License (MIT)

10 |
Copyright (c) 2013-2014 Claudio Mignanti and lwp community
11 |
Copyright (c) 2013 Antoine TANZILLI, Élie DELOUMEAU
12 | 13 |

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

14 | 15 |

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

16 | 17 |

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

18 |
19 |
20 |

Contributors

21 |
22 |
23 |
24 |
25 | 26 | {% endblock %} 27 | {% block script %} 28 | 43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /lwp/templates/checkconfig.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Config{% endblock %} 3 | {% block content %} 4 | {% if cfg %} 5 |
6 | {{ super() }} 7 | 8 | 9 | {% for i in cfg %}{% if i.split(':')[1] == 'enabled' or i.split(':')[1] == 'missing' or i.split(':')[1] == 'required' or '---' in i %} 10 | 11 | 12 | {% endif %}{% endfor %} 13 | 14 |
{% if '---' in i %}{{ i.split(':')[0] }}{% else %}{{ i.split(':')[0] }}{% endif %}
15 |
16 | 17 |
18 |
19 | 24 |
25 |
26 | 27 | {% else %} 28 |
29 | 30 |

Error!

31 |

Install kernel headers and try again...apt-get install linux-headers-$(uname -r)

32 |
33 | {% endif %} 34 | 35 | 36 | {% endblock %} -------------------------------------------------------------------------------- /lwp/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}{{container|capitalize}}{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 |

{{ container|capitalize }}

7 |
8 |
9 | {% set start_action = {'STOPPED':'start','FROZEN':'unfreeze'} %} 10 | Start 11 | Stop 12 | Freeze 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 | 37 |
38 |
39 | 40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 |
57 |
58 | 59 |
60 | 67 |
68 |
69 | 70 |
71 | 72 |
73 | 74 |
75 |
76 | 77 |
78 | 79 |
80 | 81 |
82 |
83 | 84 |
85 | 86 |
87 | 89 |
90 |
91 | 92 |
93 | 94 |
95 | 97 |
98 |
99 | 100 |
101 | 102 |
103 | 104 |
105 |
106 | 107 |
108 | 109 |
110 | 111 |
112 |
113 | 114 |
115 | 116 |
117 | 118 |
119 |
120 | 121 |
122 | 123 |
124 | 125 |
126 |
127 | 128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 | 136 | {% if settings.memlimit %}MB{% else %}Unlimited{% endif %} 137 |
138 |
139 | 140 |
141 | 142 |
143 | 144 | {% if settings.swlimit %}MB{% else %}Unlimited{% endif %} 145 |
146 |
147 | 148 |
149 | 150 |
151 | 152 | (e.g 0 or 0-1,3 or 0,3) 153 |
154 |
155 | 156 |
157 | 158 |
159 | 160 |
161 |
162 | 163 |
164 | 165 |
166 | 167 |
168 | 169 | (e.g /var/lib/lxc/{{container}}/rootfs) 170 |
171 |
172 | 173 |
174 | 175 |
176 | 177 |
178 | 189 | Default log level: 5 - error 190 |
191 |
192 | 193 |
194 | 195 |
196 | 197 | 198 |
199 |
200 | 201 |
202 |
203 | 204 |
205 | 206 | md5 like token of the backup bucket 207 |
208 |
209 | 210 |
211 |
212 | 213 |
214 |
215 | 216 |

* Set to max to unset (unlimited)
** Leave empty to unset

217 |
218 | 219 | 220 |
221 |
222 | 236 |
237 | {% include "includes/modal_clone.html" %} 238 | {% include "includes/modal_backup.html" %} 239 | {% endblock %} 240 | 241 | {% macro memory_color(value) -%}{% if value != 0 %}{% if 0 <= value <= 511 %}success{% elif 512 <= value < 980 %}warning{% else %}important{% endif %}{% endif %}{%- endmacro %} 242 | {% macro render_memory_wrapper(value) -%} 243 | {% if value != 0 %}{{ value }} MB{% endif %} 244 | {%- endmacro %} 245 | 246 | {% block script %} 247 | 295 | {% endblock %} 296 | -------------------------------------------------------------------------------- /lwp/templates/includes/aside.html: -------------------------------------------------------------------------------- 1 |
2 | 29 |
30 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_backup.html: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_clone.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_create.html: -------------------------------------------------------------------------------- 1 | 86 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_delete.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_destroy.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_new_user.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwp/templates/includes/modal_reboot.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lwp/templates/includes/nav.html: -------------------------------------------------------------------------------- 1 | 38 | -------------------------------------------------------------------------------- /lwp/templates/index.html: -------------------------------------------------------------------------------- 1 | {% set td = {'running':'success','frozen':'info','stopped':'important'} %} 2 | {% set tr = {'running':'success','frozen':'info','stopped':'error'} %} 3 | {% set disabled = {'running':'success','frozen':'info','stopped':'important'} %} 4 | {% extends "layout.html" %} 5 | {% block title %}Overview{% endblock %} 6 | {% block content %} 7 |
8 | {{ super() }} 9 | 10 |
11 |
12 | {% if session.su == 'Yes' %} 13 | Reboot 14 | {% if containers != [] %} 15 | Clone CT 16 | {% if storage_repos %} 17 | Backup CT 18 | {% endif %} 19 | {% endif %} 20 | Create CT 21 | {% endif %} 22 |
23 |
24 |

{{ dist }} ({{ host }})

25 |
26 |

CPU usage :

27 |
28 |
29 |
30 |

Memory usage :

31 |
32 |
33 |
34 |
35 |
36 |
37 |

Disk usage :

38 |
39 |
40 |
41 |

Uptime :

42 |
43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {% for status in containers_all %} 58 | 59 | {% for container in status.containers %} 60 | 61 | {% if loop.first %}{% endif %} 62 | 63 | 64 | 65 | 66 | 74 | 85 | 86 | {% endfor %} 87 | 88 | {% endfor %} 89 |
StatusNameHostnameIP AddressMem. usageAuto StartActions
{{ status.status|capitalize }}{{container.name}}{{container.settings.utsname}}{% if container.settings.ipv4 %}{{container.settings.ipv4}}{% else %}-{% endif %}{{ render_memory_wrapper(container.memusg, container.settings.memlimit) }} 67 | {% if container.settings.start_auto == '1' %} 68 | 69 | {% if container.settings.start_order %} {{ container.settings.start_order }} {% endif %} 70 | {% else %} 71 | - 72 | {% endif %} 73 | 75 |
76 |
77 | {% set start_action = {'stopped':'start','frozen':'unfreeze'} %} 78 | Start 79 | Stop 80 | Freeze 81 |
82 | {% if session.su == 'Yes' and status.status == 'stopped' %}{% endif %} 83 |
84 |
90 |
91 | {% if session.su == 'Yes' %} 92 | {% include "includes/modal_reboot.html" %} 93 | {% include "includes/modal_create.html" %} 94 | 95 | {% if containers != [] %} 96 | {% include "includes/modal_clone.html" %} 97 | {% include "includes/modal_backup.html" %} 98 | {% include "includes/modal_destroy.html" %} 99 | {% endif %} 100 | {% endif %} 101 | 102 | {% endblock %} 103 | 104 | {% macro memory_color(value) -%}{% if value != 0 %}{% if 0 <= value <= 511 %}success{% elif 512 <= value < 980 %}warning{% else %}important{% endif %}{% endif %}{%- endmacro %} 105 | {% macro render_memory_wrapper(value, limit) -%} 106 | {% if value != 0 %} 107 | {{ value }}{% if limit != '' %} / {{ limit }}{% endif %} MB 108 | {% else %} 109 | 110 | {% endif %} 111 | {%- endmacro %} 112 | 113 | {% block script %} 114 | 233 | {% endblock %} 234 | -------------------------------------------------------------------------------- /lwp/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} - LXC Web Panel 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | {% if session.logged_in %} 22 | {% include "includes/nav.html" %} 23 | {% endif %} 24 | 25 | 26 |
27 |
28 | {% if session.logged_in %} 29 | {% include "includes/aside.html" %} 30 | {% endif %} 31 | 32 | {% block content %} 33 | {% with messages = get_flashed_messages(with_categories=true) %} 34 | {% if messages %} 35 | {% for category, message in messages %} 36 |
37 | 38 | {{ message }} 39 |
40 | {% endfor %} 41 | {% endif %} 42 | {% endwith %} 43 | {% endblock %} 44 |
45 |
46 | 47 |
48 |
49 | 50 | 55 | 56 | 58 | 59 | 60 | 73 | {% block script %}{% endblock %} 74 | 75 | 76 | -------------------------------------------------------------------------------- /lwp/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Login{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 | 34 |
35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /lwp/templates/lxc-net.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Networking{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 |
7 |

LXC Network

8 |
9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 | {% if running != [] %}

Please, stop all containers before restarting lxc-net.


{% endif %} 17 | 18 | 19 |
20 |
21 | 22 |
23 | 24 | (e.g lxcbr0) 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 | (e.g 10.0.3.1) 33 |
34 |
35 | 36 |
37 | 38 |
39 | 40 | (e.g 255.255.255.0) 41 |
42 |
43 | 44 |
45 | 46 |
47 | 48 | (e.g 10.0.3.0/24) 49 |
50 |
51 | 52 |
53 | 54 |
55 | 56 | (e.g 10.0.3.2,10.0.3.254) 57 |
58 |
59 | 60 |
61 | 62 |
63 | 64 | (e.g 253) 65 |
66 |
67 |
68 |
69 |
70 | 71 | If you change these settings, don't forget to change containers addresses! 72 |
73 |
74 |
75 | 76 |
77 | {% endblock %} 78 | {% block script %} 79 | 101 | {% endblock %} 102 | -------------------------------------------------------------------------------- /lwp/templates/tokens.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Token management{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 |

On this page you can review, add and revoke API tokens.

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {% for token in tokens %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 | 26 |
TokenDescriptionAuthorized by
{{ token.token }}{{ token.description }}{{ token.username }}
27 | 28 |
29 | 30 | 31 | 32 | 33 | 34 |
35 | 36 |
37 | {% endblock %} 38 | {% block script %} 39 | 47 | {% endblock %} 48 | -------------------------------------------------------------------------------- /lwp/templates/users.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block title %}Users{% endblock %} 3 | {% block content %} 4 |
5 | {{ super() }} 6 |
7 | {% for user in users %} 8 |
9 | 15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 |

26 | 27 | 28 | 29 | 30 | 31 |
32 |
33 |
34 |
35 | {% endfor %} 36 |
37 |
38 |
39 | New user 40 |
41 | {% include "includes/modal_new_user.html" %} 42 | {% if nb_users.num > 1 %}{% include "includes/modal_delete.html" %}{% endif %} 43 | {% endblock %} 44 | {% block script %} 45 | {% if nb_users.num > 1 %} 46 | 57 | {% endif %} 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /lwp/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | import hashlib 4 | import sqlite3 5 | import ConfigParser 6 | 7 | from flask import session, render_template, g, flash, request, jsonify 8 | 9 | 10 | """ 11 | cgroup_ext is a data structure where for each input of edit.html we have an array with: 12 | position 0: the lxc container option to be saved on file 13 | position 1: the regex to validate the field 14 | position 2: the flash message to display on success. 15 | """ 16 | ip_regex = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$' 17 | cidr_regex = '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(\d|[1-2]\d|3[0-2]))*$' 18 | file_match = '^\/\w[\w.\/-]+$' 19 | 20 | cgroup_ext = { 21 | 'arch': ['lxc.arch', '^(x86|i686|x86_64|amd64)$', ''], 22 | 'utsname': ['lxc.utsname', '^\w[\w.-]+$', 'Hostname updated'], 23 | 'type': ['lxc.network.type', '^(none|empty|veth|vlan|macvlan|phys)$', 'Link network type updated'], 24 | 'link': ['lxc.network.link', '^[\w.-/]+$', 'Link name updated'], 25 | 'flags': ['lxc.network.flags', '^(up|down)$', 'Network flag updated'], 26 | 'hwaddr': ['lxc.network.hwaddr', '^[0-9a-fA-F:]+$', 'Hardware address updated'], 27 | 'ipv4': ['lxc.network.ipv4', cidr_regex, 'IPv4 address updated'], 28 | 'ipv4gw': ['lxc.network.ipv4.gateway', ip_regex, 'IPv4 gateway address updated'], 29 | 'ipv6': ['lxc.network.ipv6', '^([0-9a-fA-F:/]+)+$', 'IPv6 address updated'], # weak ipv6 regex check 30 | 'ipv6gw': ['lxc.network.ipv6.gateway', '^([0-9a-fA-F:]+)+$', 'IPv6 gateway address updated'], 31 | 'script_up': ['lxc.network.script.up', file_match, 'Network script down updated'], 32 | 'script_down': ['lxc.network.script.down', file_match, 'Network script down updated'], 33 | 'rootfs': ['lxc.rootfs', '^(\/|loop:\/|overlayfs:\/)[\w.\/:-]+$', 'Rootfs updated'], 34 | 'memlimit': ['lxc.cgroup.memory.limit_in_bytes', '^([0-9]+|)$', 'Memory limit updated'], 35 | 'swlimit': ['lxc.cgroup.memory.memsw.limit_in_bytes', '^([0-9]+|)$', 'Swap limit updated'], 36 | 'cpus': ['lxc.cgroup.cpuset.cpus', '^[0-9,-]+$', 'CPUs updated'], 37 | 'shares': ['lxc.cgroup.cpu.shares', '^[0-9]+$', 'CPU shares updated'], 38 | 'deny': ['lxc.cgroup.devices.deny', '^$', '???'], 39 | 'allow': ['lxc.cgroup.devices.allow', '^$', '???'], 40 | 'loglevel': ['lxc.loglevel', '^[0-9]$', 'Log level updated'], 41 | 'logfile': ['lxc.logfile', file_match, 'Log file updated'], 42 | 'id_map': ['lxc.id_map', '^[ug0-9 ]+$', 'UID Mapping updated'], 43 | 'hook_pre_start': ['lxc.hook.pre-start', file_match, 'Pre hook start updated'], 44 | 'hook_pre_mount': ['lxc.hook.pre-mount', file_match, 'Pre mount hook updated'], 45 | 'hook_mount': ['lxc.hook.mount', file_match, 'Mount hook updated'], 46 | 'hook_start': ['lxc.hook.start', file_match, 'Container start hook updated'], 47 | 'hook_post_stop': ['lxc.hook.post-stop', file_match, 'Container post hook updated'], 48 | 'hook_clone': ['lxc.hook.clone', file_match, 'Container clone hook updated'], 49 | 'start_auto': ['lxc.start.auto', '^(0|1)$', 'Autostart saved'], 50 | 'start_delay': ['lxc.start.delay', '^[0-9]*$', 'Autostart delay option updated'], 51 | 'start_order': ['lxc.start.order', '^[0-9]*$', 'Autostart order option updated'] 52 | } 53 | 54 | 55 | # configuration 56 | config = ConfigParser.SafeConfigParser() 57 | 58 | 59 | def read_config_file(): 60 | try: 61 | # TODO: should really use with statement here rather than rely on cpython reference counting 62 | config.readfp(open('/etc/lwp/lwp.conf')) 63 | except: 64 | # TODO: another blind exception 65 | print(' * missed /etc/lwp/lwp.conf file') 66 | try: 67 | # fallback on local config file 68 | config.readfp(open('lwp.conf')) 69 | except: 70 | print(' * cannot read config files. Exit!') 71 | sys.exit(1) 72 | return config 73 | 74 | 75 | def connect_db(db_path): 76 | """ 77 | SQLite3 connect function 78 | """ 79 | return sqlite3.connect(db_path) 80 | 81 | 82 | def query_db(query, args=(), one=False): 83 | cur = g.db.execute(query, args) 84 | rv = [dict((cur.description[idx][0], value) for idx, value in enumerate(row)) for row in cur.fetchall()] 85 | return (rv[0] if rv else None) if one else rv 86 | 87 | 88 | def if_logged_in(function=render_template, f_args=('login.html', )): 89 | """ 90 | helper decorator to verify if a user is logged 91 | """ 92 | def decorator(handler): 93 | def new_handler(*args, **kwargs): 94 | if 'logged_in' in session: 95 | return handler(*args, **kwargs) 96 | else: 97 | token = request.headers.get('Private-Token') 98 | result = query_db('select * from api_tokens where token=?', [token], one=True) 99 | if result is not None: 100 | # token exists, access granted 101 | return handler(*args, **kwargs) 102 | return function(*f_args) 103 | new_handler.func_name = handler.func_name 104 | return new_handler 105 | return decorator 106 | 107 | 108 | def get_bucket_token(container): 109 | query = query_db("SELECT bucket_token FROM machine WHERE machine_name=?", [container], one=True) 110 | if query is None: 111 | return "" 112 | else: 113 | return query['bucket_token'] 114 | 115 | 116 | def hash_passwd(passwd): 117 | return hashlib.sha512(passwd).hexdigest() 118 | 119 | 120 | def get_token(): 121 | return hashlib.md5(str(time.time())).hexdigest() 122 | 123 | 124 | def check_session_limit(): 125 | if 'logged_in' in session and session.get('last_activity') is not None: 126 | now = int(time.time()) 127 | limit = now - 60 * int(config.get('session', 'time')) 128 | last_activity = session.get('last_activity') 129 | if last_activity < limit: 130 | flash(u'Session timed out !', 'info') 131 | session.pop('logged_in', None) 132 | session.pop('token', None) 133 | session.pop('last_activity', None) 134 | session.pop('username', None) 135 | session.pop('name', None) 136 | session.pop('su', None) 137 | flash(u'You are logged out!', 'success') 138 | else: 139 | session['last_activity'] = now 140 | 141 | 142 | def api_auth(): 143 | """ 144 | api decorator to verify if a token is valid 145 | """ 146 | def decorator(handler): 147 | def new_handler(*args, **kwargs): 148 | token = request.args.get('private_token') 149 | if token is None: 150 | token = request.headers.get('Private-Token') 151 | if token: 152 | result = query_db('select * from api_tokens where token=?', [token], one=True) 153 | if result is not None: 154 | # token exists, access granted 155 | return handler(*args, **kwargs) 156 | else: 157 | return jsonify(status="error", error="Unauthorized"), 401 158 | else: 159 | return jsonify(status="error", error="Unauthorized"), 401 160 | new_handler.func_name = handler.func_name 161 | return new_handler 162 | return decorator 163 | -------------------------------------------------------------------------------- /lwp/version: -------------------------------------------------------------------------------- 1 | git 2 | -------------------------------------------------------------------------------- /lwp/views/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/claudyus/LXC-Web-Panel/a5acca4115dfdd738832b57fefe196d92cd6d029/lwp/views/__init__.py -------------------------------------------------------------------------------- /lwp/views/api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function 3 | 4 | import json 5 | 6 | from flask import Blueprint, request, g, jsonify 7 | 8 | import lwp.lxclite as lxc 9 | from lwp.utils import api_auth 10 | 11 | # Flask module 12 | mod = Blueprint('api', __name__) 13 | 14 | 15 | @mod.route('/api/v1/containers/') 16 | @api_auth() 17 | def get_containers(): 18 | """ 19 | Returns lxc containers on the current machine and brief status information. 20 | """ 21 | list_container = lxc.list_status() 22 | return json.dumps(list_container) 23 | 24 | 25 | @mod.route('/api/v1/containers/') 26 | @api_auth() 27 | def get_container(name): 28 | return jsonify(lxc.info(name)) 29 | 30 | 31 | @mod.route('/api/v1/containers/', methods=['POST']) 32 | @api_auth() 33 | def post_container(name): 34 | data = request.get_json(force=True) 35 | if data is None: 36 | return jsonify(status="error", error="Bad request"), 400 37 | 38 | status = data['action'] 39 | try: 40 | if status == "stop": 41 | lxc.stop(name) 42 | return jsonify(status="ok"), 200 43 | elif status == "start": 44 | lxc.start(name) 45 | return jsonify(status="ok"), 200 46 | elif status == "freeze": 47 | lxc.freeze(name) 48 | return jsonify(status="ok"), 200 49 | 50 | return jsonify(status="error", error="Bad request"), 400 51 | except lxc.ContainerDoesntExists: 52 | return jsonify(status="error", error="Container doesn' t exists"), 409 53 | 54 | 55 | @mod.route('/api/v1/containers/', methods=['PUT']) 56 | @api_auth() 57 | def add_container(): 58 | data = request.get_json(force=True) 59 | if data is None: 60 | return jsonify(status="error", error="Bad request"), 400 61 | 62 | if (not(('template' in data) or ('clone' in data)) or ('name' not in data)): 63 | return jsonify(status="error", error="Bad request"), 402 64 | 65 | if 'template' in data: 66 | # we want a new container 67 | if 'store' not in data: 68 | data['store'] = "" 69 | if 'xargs' not in data: 70 | data['xargs'] = "" 71 | 72 | try: 73 | lxc.create(data['name'], data['template'], data['store'], data['xargs']) 74 | except lxc.ContainerAlreadyExists: 75 | return jsonify(status="error", error="Container yet exists"), 409 76 | else: 77 | # we want to clone a container 78 | try: 79 | lxc.clone(data['clone'], data['name']) 80 | except lxc.ContainerAlreadyExists: 81 | return jsonify(status="error", error="Container yet exists"), 409 82 | return jsonify(status="ok"), 200 83 | 84 | 85 | @mod.route('/api/v1/containers/', methods=['DELETE']) 86 | @api_auth() 87 | def delete_container(name): 88 | try: 89 | lxc.destroy(name) 90 | return jsonify(status="ok"), 200 91 | except lxc.ContainerDoesntExists: 92 | return jsonify(status="error", error="Container doesn' t exists"), 400 93 | 94 | 95 | @mod.route('/api/v1/tokens/', methods=['POST']) 96 | @api_auth() 97 | def add_token(): 98 | data = request.get_json(force=True) 99 | if data is None or 'token' not in data: 100 | return jsonify(status="error", error="Bad request"), 400 101 | 102 | if 'description' not in data: 103 | data.update(description="no description") 104 | g.db.execute('insert into api_tokens(description, token) values(?, ?)', [data['description'], data['token']]) 105 | g.db.commit() 106 | return jsonify(status="ok"), 200 107 | 108 | 109 | @mod.route('/api/v1/tokens/', methods=['DELETE']) 110 | @api_auth() 111 | def delete_token(token): 112 | g.db.execute('delete from api_tokens where token=?', [token]) 113 | g.db.commit() 114 | return jsonify(status="ok"), 200 115 | -------------------------------------------------------------------------------- /lwp/views/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function 3 | 4 | import time 5 | 6 | from flask import Blueprint, request, session, redirect, url_for, render_template, flash 7 | 8 | from lwp.utils import get_token, read_config_file 9 | 10 | import lwp.authenticators as auth 11 | 12 | AUTH = read_config_file().get('global', 'auth') 13 | AUTH_INSTANCE = auth.get_authenticator(AUTH) 14 | print(' * Auth type: ' + AUTH) 15 | 16 | 17 | # Flask module 18 | mod = Blueprint('auth', __name__) 19 | 20 | 21 | @mod.route('/login', methods=['GET', 'POST']) 22 | def login(): 23 | if request.method == 'POST': 24 | request_username = request.form['username'] 25 | request_passwd = request.form['password'] 26 | 27 | current_url = request.form['url'] 28 | 29 | user = AUTH_INSTANCE.authenticate(request_username, request_passwd) 30 | 31 | if user: 32 | session['logged_in'] = True 33 | session['token'] = get_token() 34 | session['last_activity'] = int(time.time()) 35 | session['username'] = user['username'] 36 | session['name'] = user['name'] 37 | session['su'] = user['su'] 38 | flash(u'You are logged in!', 'success') 39 | 40 | if current_url == url_for('auth.login'): 41 | return redirect(url_for('main.home')) 42 | return redirect(current_url) 43 | 44 | flash(u'Invalid username or password!', 'error') 45 | return render_template('login.html', auth=AUTH) 46 | 47 | 48 | @mod.route('/logout') 49 | def logout(): 50 | session.pop('logged_in', None) 51 | session.pop('token', None) 52 | session.pop('last_activity', None) 53 | session.pop('username', None) 54 | session.pop('name', None) 55 | session.pop('su', None) 56 | flash(u'You are logged out!', 'success') 57 | return redirect(url_for('auth.login')) 58 | -------------------------------------------------------------------------------- /lwp/views/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import, print_function 3 | import os 4 | import re 5 | import time 6 | import socket 7 | import subprocess 8 | import ConfigParser 9 | 10 | from flask import Blueprint, request, session, g, redirect, url_for, abort, render_template, flash, jsonify 11 | 12 | import lwp 13 | import lwp.lxclite as lxc 14 | from lwp.utils import query_db, if_logged_in, get_bucket_token, hash_passwd, read_config_file, cgroup_ext 15 | from lwp.views.auth import AUTH 16 | 17 | # TODO: see if we can move this block somewhere better 18 | try: 19 | config = read_config_file() 20 | USE_BUCKET = config.getboolean('global', 'buckets') 21 | BUCKET_HOST = config.get('buckets', 'buckets_host') 22 | BUCKET_PORT = config.get('buckets', 'buckets_port') 23 | except ConfigParser.NoOptionError: 24 | USE_BUCKET = False 25 | print("- Bucket feature disabled") 26 | 27 | 28 | storage_repos = config.items('storage_repository') 29 | 30 | # Flask module 31 | mod = Blueprint('main', __name__) 32 | 33 | 34 | @mod.route('/') 35 | @mod.route('/home') 36 | @if_logged_in() 37 | def home(): 38 | """ 39 | home page function 40 | """ 41 | listx = lxc.listx() 42 | containers_all = [] 43 | 44 | for status in ('RUNNING', 'FROZEN', 'STOPPED'): 45 | containers_by_status = [] 46 | 47 | for container in listx[status]: 48 | container_info = { 49 | 'name': container, 50 | 'settings': lwp.get_container_settings(container, status), 51 | 'memusg': 0, 52 | 'bucket': get_bucket_token(container) 53 | } 54 | 55 | containers_by_status.append(container_info) 56 | containers_all.append({ 57 | 'status': status.lower(), 58 | 'containers': containers_by_status 59 | }) 60 | clonable_containers = listx['STOPPED'] 61 | 62 | return render_template('index.html', containers=lxc.ls(), containers_all=containers_all, dist=lwp.name_distro(), 63 | host=socket.gethostname(), templates=lwp.get_templates_list(), storage_repos=storage_repos, 64 | auth=AUTH, clonable_containers=clonable_containers) 65 | 66 | 67 | @mod.route('/about') 68 | @if_logged_in() 69 | def about(): 70 | """ 71 | about page 72 | """ 73 | return render_template('about.html', containers=lxc.ls(), version=lwp.check_version()) 74 | 75 | 76 | @mod.route('//edit', methods=['POST', 'GET']) 77 | @if_logged_in() 78 | def edit(container=None): 79 | """ 80 | edit containers page and actions if form post request 81 | """ 82 | host_memory = lwp.host_memory_usage() 83 | cfg = lwp.get_container_settings(container) 84 | 85 | if request.method == 'POST': 86 | form = request.form.copy() 87 | 88 | if form['bucket'] != get_bucket_token(container): 89 | g.db.execute("INSERT INTO machine(machine_name, bucket_token) VALUES (?, ?)", [container, form['bucket']]) 90 | g.db.commit() 91 | flash(u'Bucket config for %s saved' % container, 'success') 92 | 93 | # convert boolean in correct value for lxc, if checkbox is inset value is not submitted inside POST 94 | form['flags'] = 'up' if 'flags' in form else 'down' 95 | form['start_auto'] = '1' if 'start_auto' in form else '0' 96 | 97 | # if memlimits/memswlimit is at max values unset form values 98 | if int(form['memlimit']) == host_memory['total']: 99 | form['memlimit'] = '' 100 | if int(form['swlimit']) == host_memory['total'] * 2: 101 | form['swlimit'] = '' 102 | 103 | for option in form.keys(): 104 | # if the key is supported AND is different 105 | if option in cfg.keys() and form[option] != cfg[option]: 106 | # validate value with regex 107 | if re.match(cgroup_ext[option][1], form[option]): 108 | lwp.push_config_value(cgroup_ext[option][0], form[option], container=container) 109 | flash(cgroup_ext[option][2], 'success') 110 | else: 111 | flash('Cannot validate value for option {}. Unsaved!'.format(option), 'error') 112 | 113 | # we should re-read container configuration now to be coherent with the newly saved values 114 | cfg = lwp.get_container_settings(container) 115 | 116 | info = lxc.info(container) 117 | infos = {'status': info['state'], 'pid': info['pid'], 'memusg': lwp.memory_usage(container)} 118 | 119 | # prepare a regex dict from cgroups_ext definition 120 | regex = {} 121 | for k, v in cgroup_ext.items(): 122 | regex[k] = v[1] 123 | 124 | return render_template('edit.html', containers=lxc.ls(), container=container, infos=infos, 125 | settings=cfg, host_memory=host_memory, storage_repos=storage_repos, regex=regex, 126 | clonable_containers=lxc.listx()['STOPPED']) 127 | 128 | 129 | @mod.route('/settings/lxc-net', methods=['POST', 'GET']) 130 | @if_logged_in() 131 | def lxc_net(): 132 | """ 133 | lxc-net (/etc/default/lxc) settings page and actions if form post request 134 | """ 135 | if session['su'] != 'Yes': 136 | return abort(403) 137 | 138 | if request.method == 'POST': 139 | if lxc.running() == []: 140 | cfg = lwp.get_net_settings() 141 | ip_regex = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' 142 | 143 | form = {} 144 | for key in ['bridge', 'address', 'netmask', 'network', 'range', 'max']: 145 | form[key] = request.form.get(key, None) 146 | form['use'] = request.form.get('use', None) 147 | 148 | if form['use'] != cfg['use']: 149 | lwp.push_net_value('USE_LXC_BRIDGE', 'true' if form['use'] else 'false') 150 | 151 | if form['bridge'] and form['bridge'] != cfg['bridge'] and \ 152 | re.match('^[a-zA-Z0-9_-]+$', form['bridge']): 153 | lwp.push_net_value('LXC_BRIDGE', form['bridge']) 154 | 155 | if form['address'] and form['address'] != cfg['address'] and \ 156 | re.match('^%s$' % ip_regex, form['address']): 157 | lwp.push_net_value('LXC_ADDR', form['address']) 158 | 159 | if form['netmask'] and form['netmask'] != cfg['netmask'] and \ 160 | re.match('^%s$' % ip_regex, form['netmask']): 161 | lwp.push_net_value('LXC_NETMASK', form['netmask']) 162 | 163 | if form['network'] and form['network'] != cfg['network'] and \ 164 | re.match('^%s(?:/\d{1,2}|)$' % ip_regex, form['network']): 165 | lwp.push_net_value('LXC_NETWORK', form['network']) 166 | 167 | if form['range'] and form['range'] != cfg['range'] and \ 168 | re.match('^%s,%s$' % (ip_regex, ip_regex), form['range']): 169 | lwp.push_net_value('LXC_DHCP_RANGE', form['range']) 170 | 171 | if form['max'] and form['max'] != cfg['max'] and \ 172 | re.match('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', form['max']): 173 | lwp.push_net_value('LXC_DHCP_MAX', form['max']) 174 | 175 | if lwp.net_restart() == 0: 176 | flash(u'LXC Network settings applied successfully!', 'success') 177 | else: 178 | flash(u'Failed to restart LXC networking.', 'error') 179 | else: 180 | flash(u'Stop all containers before restart lxc-net.', 'warning') 181 | return render_template('lxc-net.html', containers=lxc.ls(), cfg=lwp.get_net_settings(), running=lxc.running()) 182 | 183 | 184 | @mod.route('/lwp/users', methods=['POST', 'GET']) 185 | @if_logged_in() 186 | def lwp_users(): 187 | """ 188 | returns users and get posts request : can edit or add user in page. 189 | this funtction uses sqlite3 190 | """ 191 | if session['su'] != 'Yes': 192 | return abort(403) 193 | 194 | if AUTH != 'database': 195 | return abort(403, 'You are using an auth method other that database.') 196 | 197 | try: 198 | trash = request.args.get('trash') 199 | except KeyError: 200 | trash = 0 201 | 202 | su_users = query_db("SELECT COUNT(id) as num FROM users WHERE su='Yes'", [], one=True) 203 | 204 | if request.args.get('token') == session.get('token') and int(trash) == 1 and request.args.get('userid') and \ 205 | request.args.get('username'): 206 | nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) 207 | 208 | if nb_users['num'] > 1: 209 | if su_users['num'] <= 1: 210 | su_user = query_db("SELECT username FROM users WHERE su='Yes'", [], one=True) 211 | 212 | if su_user['username'] == request.args.get('username'): 213 | flash(u'Can\'t delete the last admin user : %s' % request.args.get('username'), 'error') 214 | return redirect(url_for('main.lwp_users')) 215 | 216 | g.db.execute("DELETE FROM users WHERE id=? AND username=?", [request.args.get('userid'), 217 | request.args.get('username')]) 218 | g.db.commit() 219 | flash(u'Deleted %s' % request.args.get('username'), 'success') 220 | return redirect(url_for('main.lwp_users')) 221 | 222 | flash(u'Can\'t delete the last user!', 'error') 223 | return redirect(url_for('main.lwp_users')) 224 | 225 | if request.method == 'POST': 226 | users = query_db('SELECT id, name, username, su FROM users ORDER BY id ASC') 227 | 228 | if request.form['newUser'] == 'True': 229 | if not request.form['username'] in [user['username'] for user in users]: 230 | if re.match('^\w+$', request.form['username']) and request.form['password1']: 231 | if request.form['password1'] == request.form['password2']: 232 | if request.form['name']: 233 | if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): 234 | g.db.execute("INSERT INTO users (name, username, password) VALUES (?, ?, ?)", 235 | [request.form['name'], request.form['username'], 236 | hash_passwd(request.form['password1'])]) 237 | g.db.commit() 238 | else: 239 | flash(u'Invalid name!', 'error') 240 | else: 241 | g.db.execute("INSERT INTO users (username, password) VALUES (?, ?)", 242 | [request.form['username'], hash_passwd(request.form['password1'])]) 243 | g.db.commit() 244 | 245 | flash(u'Created %s' % request.form['username'], 'success') 246 | else: 247 | flash(u'No password match', 'error') 248 | else: 249 | flash(u'Invalid username or password!', 'error') 250 | else: 251 | flash(u'Username already exist!', 'error') 252 | 253 | elif request.form['newUser'] == 'False': 254 | if request.form['password1'] == request.form['password2']: 255 | if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): 256 | if su_users['num'] <= 1: 257 | su = 'Yes' 258 | else: 259 | try: 260 | su = request.form['su'] 261 | except KeyError: 262 | su = 'No' 263 | 264 | if not request.form['name']: 265 | g.db.execute("UPDATE users SET name='', su=? WHERE username=?", [su, request.form['username']]) 266 | g.db.commit() 267 | elif request.form['name'] and not request.form['password1'] and not request.form['password2']: 268 | g.db.execute("UPDATE users SET name=?, su=? WHERE username=?", 269 | [request.form['name'], su, request.form['username']]) 270 | g.db.commit() 271 | elif request.form['name'] and request.form['password1'] and request.form['password2']: 272 | g.db.execute("UPDATE users SET name=?, password=?, su=? WHERE username=?", 273 | [request.form['name'], hash_passwd(request.form['password1']), su, 274 | request.form['username']]) 275 | g.db.commit() 276 | elif request.form['password1'] and request.form['password2']: 277 | g.db.execute("UPDATE users SET password=?, su=? WHERE username=?", 278 | [hash_passwd(request.form['password1']), su, request.form['username']]) 279 | g.db.commit() 280 | 281 | flash(u'Updated', 'success') 282 | else: 283 | flash(u'Invalid name!', 'error') 284 | else: 285 | flash(u'No password match', 'error') 286 | else: 287 | flash(u'Unknown error!', 'error') 288 | 289 | users = query_db("SELECT id, name, username, su FROM users ORDER BY id ASC") 290 | nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) 291 | su_users = query_db("SELECT COUNT(id) as num FROM users WHERE su='Yes'", [], one=True) 292 | 293 | return render_template('users.html', containers=lxc.ls(), users=users, nb_users=nb_users, su_users=su_users) 294 | 295 | 296 | @mod.route('/lwp/tokens', methods=['POST', 'GET']) 297 | @if_logged_in() 298 | def lwp_tokens(): 299 | """ 300 | returns api tokens info and get posts request: can show/delete or add token in page. 301 | this function uses sqlite3, require admin privilege 302 | """ 303 | if session['su'] != 'Yes': 304 | return abort(403) 305 | 306 | if request.method == 'POST': 307 | if request.form['action'] == 'add': 308 | # we want to add a new token 309 | token = request.form['token'] 310 | description = request.form['description'] 311 | username = session['username'] # we should save the username due to ldap option 312 | g.db.execute("INSERT INTO api_tokens (username, token, description) VALUES(?, ?, ?)", [username, token, 313 | description]) 314 | g.db.commit() 315 | flash(u'Token %s successfully added!' % token, 'success') 316 | 317 | if request.args.get('action') == 'del': 318 | token = request.args['token'] 319 | g.db.execute("DELETE FROM api_tokens WHERE token=?", [token]) 320 | g.db.commit() 321 | flash(u'Token %s successfully deleted!' % token, 'success') 322 | 323 | tokens = query_db("SELECT description, token, username FROM api_tokens ORDER BY token DESC") 324 | return render_template('tokens.html', containers=lxc.ls(), tokens=tokens) 325 | 326 | 327 | @mod.route('/checkconfig') 328 | @if_logged_in() 329 | def checkconfig(): 330 | """ 331 | returns the display of lxc-checkconfig command 332 | """ 333 | if session['su'] != 'Yes': 334 | return abort(403) 335 | 336 | return render_template('checkconfig.html', containers=lxc.ls(), cfg=lxc.checkconfig()) 337 | 338 | 339 | @mod.route('/action', methods=['GET']) 340 | @if_logged_in() 341 | def action(): 342 | """ 343 | manage all actions related to containers 344 | lxc-start, lxc-stop, etc... 345 | """ 346 | act = request.args['action'] 347 | name = request.args['name'] 348 | 349 | # TODO: refactor this method, it's horrible to read 350 | if act == 'start': 351 | try: 352 | if lxc.start(name) == 0: 353 | time.sleep(1) # Fix bug : "the container is randomly not displayed in overview list after a boot" 354 | flash(u'Container %s started successfully!' % name, 'success') 355 | else: 356 | flash(u'Unable to start %s!' % name, 'error') 357 | except lxc.ContainerAlreadyRunning: 358 | flash(u'Container %s is already running!' % name, 'error') 359 | elif act == 'stop': 360 | try: 361 | if lxc.stop(name) == 0: 362 | flash(u'Container %s stopped successfully!' % name, 'success') 363 | else: 364 | flash(u'Unable to stop %s!' % name, 'error') 365 | except lxc.ContainerNotRunning: 366 | flash(u'Container %s is already stopped!' % name, 'error') 367 | elif act == 'freeze': 368 | try: 369 | if lxc.freeze(name) == 0: 370 | flash(u'Container %s frozen successfully!' % name, 'success') 371 | else: 372 | flash(u'Unable to freeze %s!' % name, 'error') 373 | except lxc.ContainerNotRunning: 374 | flash(u'Container %s not running!' % name, 'error') 375 | elif act == 'unfreeze': 376 | try: 377 | if lxc.unfreeze(name) == 0: 378 | flash(u'Container %s unfrozen successfully!' % name, 'success') 379 | else: 380 | flash(u'Unable to unfeeze %s!' % name, 'error') 381 | except lxc.ContainerNotRunning: 382 | flash(u'Container %s not frozen!' % name, 'error') 383 | elif act == 'destroy': 384 | if session['su'] != 'Yes': 385 | return abort(403) 386 | try: 387 | if lxc.destroy(name) == 0: 388 | flash(u'Container %s destroyed successfully!' % name, 'success') 389 | else: 390 | flash(u'Unable to destroy %s!' % name, 'error') 391 | except lxc.ContainerDoesntExists: 392 | flash(u'The Container %s does not exists!' % name, 'error') 393 | elif act == 'reboot' and name == 'host': 394 | if session['su'] != 'Yes': 395 | return abort(403) 396 | msg = '\v*** LXC Web Panel *** \ 397 | \nReboot from web panel' 398 | try: 399 | subprocess.check_call('/sbin/shutdown -r now \'%s\'' % msg, shell=True) 400 | flash(u'System will now restart!', 'success') 401 | except subprocess.CalledProcessError: 402 | flash(u'System error!', 'error') 403 | elif act == 'push': 404 | # TODO: implement push action 405 | pass 406 | try: 407 | if request.args['from'] == 'edit': 408 | return redirect(url_for('main.edit', container=name)) 409 | else: 410 | return redirect(url_for('main.home')) 411 | except KeyError: 412 | return redirect(url_for('main.home')) 413 | 414 | 415 | @mod.route('/action/create-container', methods=['GET', 'POST']) 416 | @if_logged_in() 417 | def create_container(): 418 | """ 419 | verify all forms to create a container 420 | """ 421 | if session['su'] != 'Yes': 422 | return abort(403) 423 | if request.method == 'POST': 424 | name = request.form['name'] 425 | template = request.form['template'] 426 | command = request.form['command'] 427 | 428 | if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): 429 | storage_method = request.form['backingstore'] 430 | 431 | if storage_method == 'default': 432 | try: 433 | if lxc.create(name, template=template, xargs=command) == 0: 434 | flash(u'Container %s created successfully!' % name, 'success') 435 | else: 436 | flash(u'Failed to create %s!' % name, 'error') 437 | except lxc.ContainerAlreadyExists: 438 | flash(u'The Container %s is already created!' % name, 'error') 439 | except subprocess.CalledProcessError: 440 | flash(u'Error! %s' % name, 'error') 441 | 442 | elif storage_method == 'directory': 443 | directory = request.form['dir'] 444 | 445 | if re.match('^/[a-zA-Z0-9_/-]+$', directory) and directory != '': 446 | try: 447 | if lxc.create(name, template=template, storage='dir --dir %s' % directory, xargs=command) == 0: 448 | flash(u'Container %s created successfully!' % name, 'success') 449 | else: 450 | flash(u'Failed to create %s!' % name, 'error') 451 | except lxc.ContainerAlreadyExists: 452 | flash(u'The Container %s is already created!' % name, 'error') 453 | except subprocess.CalledProcessError: 454 | flash(u'Error! %s' % name, 'error') 455 | 456 | elif storage_method == 'btrfs': 457 | try: 458 | if lxc.create(name, template=template, storage='btrfs', xargs=command) == 0: 459 | flash(u'Container %s created successfully!' % name, 'success') 460 | else: 461 | flash(u'Failed to create %s!' % name, 'error') 462 | except lxc.ContainerAlreadyExists: 463 | flash(u'The Container %s is already created!' % name, 'error') 464 | except subprocess.CalledProcessError: 465 | flash(u'Error! %s' % name, 'error') 466 | 467 | elif storage_method == 'zfs': 468 | zfs = request.form['zpoolname'] 469 | 470 | if re.match('^[a-zA-Z0-9_-]+$', zfs) and zfs != '': 471 | try: 472 | if lxc.create(name, template=template, storage='zfs --zfsroot %s' % zfs, xargs=command) == 0: 473 | flash(u'Container %s created successfully!' % name, 'success') 474 | else: 475 | flash(u'Failed to create %s!' % name, 'error') 476 | except lxc.ContainerAlreadyExists: 477 | flash(u'The Container %s is already created!' % name, 'error') 478 | except subprocess.CalledProcessError: 479 | flash(u'Error! %s' % name, 'error') 480 | 481 | elif storage_method == 'lvm': 482 | lvname = request.form['lvname'] 483 | vgname = request.form['vgname'] 484 | fstype = request.form['fstype'] 485 | fssize = request.form['fssize'] 486 | storage_options = 'lvm' 487 | 488 | if re.match('^[a-zA-Z0-9_-]+$', lvname) and lvname != '': 489 | storage_options += ' --lvname %s' % lvname 490 | if re.match('^[a-zA-Z0-9_-]+$', vgname) and vgname != '': 491 | storage_options += ' --vgname %s' % vgname 492 | if re.match('^[a-z0-9]+$', fstype) and fstype != '': 493 | storage_options += ' --fstype %s' % fstype 494 | if re.match('^[0-9]+[G|M]$', fssize) and fssize != '': 495 | storage_options += ' --fssize %s' % fssize 496 | 497 | try: 498 | if lxc.create(name, template=template, storage=storage_options, xargs=command) == 0: 499 | flash(u'Container %s created successfully!' % name, 'success') 500 | else: 501 | flash(u'Failed to create %s!' % name, 'error') 502 | except lxc.ContainerAlreadyExists: 503 | flash(u'The container/logical volume %s is already created!' % name, 'error') 504 | except subprocess.CalledProcessError: 505 | flash(u'Error! %s' % name, 'error') 506 | 507 | else: 508 | flash(u'Missing parameters to create container!', 'error') 509 | 510 | else: 511 | if name == '': 512 | flash(u'Please enter a container name!', 'error') 513 | else: 514 | flash(u'Invalid name for \"%s\"!' % name, 'error') 515 | 516 | return redirect(url_for('main.home')) 517 | 518 | 519 | @mod.route('/action/clone-container', methods=['GET', 'POST']) 520 | @if_logged_in() 521 | def clone_container(): 522 | """ 523 | verify all forms to clone a container 524 | """ 525 | if session['su'] != 'Yes': 526 | return abort(403) 527 | if request.method == 'POST': 528 | orig = request.form['orig'] 529 | name = request.form['name'] 530 | 531 | try: 532 | snapshot = request.form['snapshot'] 533 | if snapshot == 'True': 534 | snapshot = True 535 | except KeyError: 536 | snapshot = False 537 | 538 | if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): 539 | out = None 540 | 541 | try: 542 | out = lxc.clone(orig=orig, new=name, snapshot=snapshot) 543 | except lxc.ContainerAlreadyExists: 544 | flash(u'The Container %s already exists!' % name, 'error') 545 | except subprocess.CalledProcessError: 546 | flash(u'Can\'t snapshot a directory', 'error') 547 | 548 | if out and out == 0: 549 | flash(u'Container %s cloned into %s successfully!' % (orig, name), 'success') 550 | elif out and out != 0: 551 | flash(u'Failed to clone %s into %s!' % (orig, name), 'error') 552 | 553 | else: 554 | if name == '': 555 | flash(u'Please enter a container name!', 'error') 556 | else: 557 | flash(u'Invalid name for \"%s\"!' % name, 'error') 558 | 559 | return redirect(url_for('main.home')) 560 | 561 | 562 | @mod.route('/action/backup-container', methods=['GET', 'POST']) 563 | @if_logged_in() 564 | def backup_container(): 565 | """ 566 | Verify the form to backup a container 567 | """ 568 | if request.method == 'POST': 569 | container = request.form['orig'] 570 | sr_type = request.form['dest'] 571 | if 'push' in request.form: 572 | push = request.form['push'] 573 | else: 574 | push = False 575 | sr_path = None 576 | for sr in storage_repos: 577 | if sr_type in sr: 578 | sr_path = sr[1] 579 | break 580 | 581 | backup_failed = True 582 | 583 | try: 584 | backup_file = lxc.backup(container=container, sr_type=sr_type, destination=sr_path) 585 | bucket_token = get_bucket_token(container) 586 | if push and bucket_token and USE_BUCKET: 587 | os.system('curl http://{}:{}/{} -F file=@{}'.format(BUCKET_HOST, BUCKET_PORT, bucket_token, backup_file)) 588 | backup_failed = False 589 | except lxc.ContainerDoesntExists: 590 | flash(u'The Container %s does not exist !' % container, 'error') 591 | except lxc.DirectoryDoesntExists: 592 | flash(u'Local backup directory "%s" does not exist !' % sr_path, 'error') 593 | except lxc.NFSDirectoryNotMounted: 594 | flash(u'NFS repository "%s" not mounted !' % sr_path, 'error') 595 | except subprocess.CalledProcessError: 596 | flash(u'Error during transfert !', 'error') 597 | except: 598 | flash(u'Error during transfert !', 'error') 599 | 600 | if backup_failed is not True: 601 | flash(u'Container %s backed up successfully' % container, 'success') 602 | else: 603 | flash(u'Failed to backup %s container' % container, 'error') 604 | 605 | return redirect(url_for('main.home')) 606 | 607 | 608 | @mod.route('/_refresh_info') 609 | @if_logged_in() 610 | def refresh_info(): 611 | return jsonify({'cpu': lwp.host_cpu_percent(), 612 | 'uptime': lwp.host_uptime(), 613 | 'disk': lwp.host_disk_usage()}) 614 | 615 | 616 | @mod.route('/_refresh_memory_') 617 | @if_logged_in() 618 | def refresh_memory_containers(name=None): 619 | if name == 'containers': 620 | containers_running = lxc.running() 621 | containers = [] 622 | for container in containers_running: 623 | container = container.replace(' (auto)', '') 624 | containers.append({'name': container, 'memusg': lwp.memory_usage(container), 625 | 'settings': lwp.get_container_settings(container)}) 626 | return jsonify(data=containers) 627 | elif name == 'host': 628 | return jsonify(lwp.host_memory_usage()) 629 | return jsonify({'memusg': lwp.memory_usage(name)}) 630 | 631 | 632 | @mod.route('/_check_version') 633 | @if_logged_in() 634 | def check_version(): 635 | return jsonify(lwp.check_version()) 636 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | flake8 2 | fabric 3 | pytz 4 | Flask-Testing 5 | mechanize 6 | coveralls 7 | nose 8 | mock 9 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | from setuptools import setup, find_packages 5 | 6 | here = os.path.abspath(os.path.dirname(__file__)) 7 | README = open(os.path.join(here, 'README.rst')).read() 8 | VERSION = open(os.path.join(here, 'lwp/version')).read() 9 | 10 | setup( 11 | name='lwp', 12 | version=VERSION, 13 | description='LXC Web Panel', 14 | long_description=README, 15 | author='Claudio Mignanti', 16 | author_email='c.mignanti@gmail.com', 17 | url='https://github.com/claudyus/LXC-Web-Panel', 18 | packages=find_packages(), 19 | include_package_data=True, 20 | zip_safe=False, 21 | install_requires=[ 22 | 'flask>=0.10', 23 | 'jinja2>=2.7.2', 24 | 'python-ldap', 25 | 'PyOpenSSL', 26 | ], 27 | scripts=['bin/lwp'], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/api.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import unittest 3 | import urllib2 4 | import shutil 5 | import json 6 | import ast 7 | import os 8 | 9 | from flask import Flask 10 | from flask.ext.testing import LiveServerTestCase 11 | 12 | from lwp.app import app 13 | from lwp.utils import connect_db 14 | 15 | token = 'myrandomapites0987' 16 | 17 | class TestApi(LiveServerTestCase): 18 | 19 | db = None 20 | type_json = {'Content-Type': 'application/json'} 21 | 22 | def create_app(self): 23 | shutil.copyfile('lwp.db', '/tmp/db.sql') 24 | self.db = connect_db('/tmp/db.sql') 25 | self.db.execute('insert into api_tokens(description, token) values(?, ?)', ['test', token]) 26 | self.db.commit() 27 | app.config['DATABASE'] = '/tmp/db.sql' 28 | return app 29 | 30 | def test_00_get_containers(self): 31 | shutil.rmtree('/tmp/lxc/', ignore_errors=True) 32 | request = urllib2.Request(self.get_server_url() + '/api/v1/containers/', 33 | headers={'Private-Token': token}) 34 | response = urllib2.urlopen(request) 35 | self.assertEqual(response.code, 200) 36 | #assert isinstance(response.read(), list) 37 | 38 | def test_01_put_containers(self): 39 | data = {'name': 'test_vm_sshd', 'template': 'sshd'} 40 | request = urllib2.Request(self.get_server_url() + '/api/v1/containers/', json.dumps(data), 41 | headers={'Private-Token': token, 'Content-Type': 'application/json' }) 42 | request.get_method = lambda: 'PUT' 43 | response = urllib2.urlopen(request) 44 | self.assertEqual(response.code, 200) 45 | assert data['name'] in os.listdir('/tmp/lxc') 46 | 47 | def test_02_post_containers(self): 48 | data = {'action': 'start'} 49 | request = urllib2.Request(self.get_server_url() + '/api/v1/containers/test_vm_sshd', json.dumps(data), 50 | headers={'Private-Token': token, 'Content-Type': 'application/json'}) 51 | request.get_method = lambda: 'POST' 52 | response = urllib2.urlopen(request) 53 | self.assertEqual(response.code, 200) 54 | 55 | def test_03_delete_containers(self): 56 | request = urllib2.Request(self.get_server_url() + '/api/v1/containers/test_vm_sshd', 57 | headers={'Private-Token': token}) 58 | request.get_method = lambda: 'DELETE' 59 | response = urllib2.urlopen(request) 60 | self.assertEqual(response.code, 200) 61 | 62 | def test_04_post_token(self): 63 | data = {'token': 'test'} 64 | request = urllib2.Request(self.get_server_url() + '/api/v1/tokens/', json.dumps(data), 65 | headers={'Private-Token': token, 'Content-Type': 'application/json'}) 66 | response = urllib2.urlopen(request) 67 | self.assertEqual(response.code, 200) 68 | 69 | def test_05_delete_token(self): 70 | request = urllib2.Request(self.get_server_url() + '/api/v1/tokens/test', 71 | headers={'Private-Token': token}) 72 | request.get_method = lambda: 'DELETE' 73 | response = urllib2.urlopen(request) 74 | self.assertEqual(response.code, 200) 75 | 76 | 77 | if __name__ == '__main__': 78 | unittest.main() 79 | -------------------------------------------------------------------------------- /tests/auth.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mock import MagicMock 4 | 5 | class TestAuths(unittest.TestCase): 6 | """ 7 | Those tests are against the auth plugins 8 | """ 9 | 10 | def test_htpasswd_passwd_auth(self): 11 | # FIXME the config mock is ovewrite by lwp.app load 12 | # align test file to default example.conf 13 | #global config 14 | #config = MagicMock() 15 | #config.get('htpasswd', 'file', return_value='/var/lwp/htpasswd') 16 | from lwp.authenticators.htpasswd import htpasswd 17 | 18 | with open('/var/lwp/htpasswd', 'w') as file_pass: 19 | file_pass.write('user_test:L2HG274hqrFwo\n') 20 | 21 | h = htpasswd() 22 | user = h.authenticate('user_test', 'pass') 23 | assert user.get('username') == 'user_test' 24 | 25 | def test_htpasswd_passwd_auth_wrongpass(self): 26 | from lwp.authenticators.htpasswd import htpasswd 27 | 28 | with open('/var/lwp/htpasswd', 'w') as file_pass: 29 | file_pass.write('user_test:L2HG274hqrFwo\n') 30 | 31 | h = htpasswd() 32 | user = h.authenticate('user_test', 'wrong_pass') 33 | assert user == None 34 | 35 | 36 | def test_http(self): 37 | from lwp.authenticators.http import http 38 | 39 | h = http() 40 | assert h.authenticate('test', 'user') 41 | 42 | if __name__ == '__main__': 43 | unittest.main() -------------------------------------------------------------------------------- /tests/browser.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import mechanize 3 | import cookielib 4 | import unittest 5 | import shutil 6 | import os 7 | 8 | from flask import Flask 9 | from flask.ext.testing import LiveServerTestCase 10 | 11 | from lwp.app import app 12 | from lwp.utils import connect_db 13 | 14 | 15 | #class TestWebBrowser(unittest.TestCase): 16 | class TestWebBrowser(LiveServerTestCase): 17 | """ 18 | These tests are made using a stateful programmatic web browsing 19 | and use the cookie and standard login form to operate on the lwp. 20 | """ 21 | 22 | @classmethod 23 | def setUpClass(cls): 24 | # cleanup 25 | shutil.copyfile('lwp.db.base', '/tmp/db.sql') 26 | shutil.rmtree('/tmp/lxc', ignore_errors=True) 27 | cj = cookielib.LWPCookieJar() 28 | cls.br = mechanize.Browser() 29 | cls.br.set_cookiejar(cj) 30 | 31 | def create_app(self): 32 | app.config['DATABASE'] = '/tmp/db.sql' 33 | return app 34 | 35 | def test_00_login(self): 36 | """ 37 | login with the standard admin/admin 38 | """ 39 | self.client = app.test_client() 40 | self.client.post('/login', data={'username': 'admin', 'password': 'admin'}, follow_redirects=True) 41 | 42 | self.br.open(self.get_server_url() + "/login") 43 | resp = self.br.response() 44 | assert self.br.viewing_html() 45 | 46 | # select login form and fill it 47 | self.br.select_form(name="form-signin") 48 | self.br['username'] = "admin" 49 | self.br['password'] = "admin" 50 | resp = self.br.submit() 51 | 52 | assert '/home' in resp.geturl() 53 | 54 | def test_01_home_render(self): 55 | """ 56 | we are now logged in, create a container and check that 57 | it is displayed in home page, the stopped badge is displayed 58 | """ 59 | subprocess.check_output('lxc-create -n mocktest_00_lxc', shell=True) 60 | 61 | self.br.open(self.get_server_url() + "/home") 62 | resp = self.br.response().read() 63 | assert self.br.viewing_html() 64 | 65 | assert 'mocktest_00_lxc' in resp 66 | assert 'Stopped' in resp 67 | 68 | def test_02_start_container(self): 69 | """ 70 | the container exists, start it using /action and check badge on home 71 | """ 72 | self.br.open(self.get_server_url() + "/action?action=start&name=mocktest_00_lxc") 73 | 74 | self.br.open(self.get_server_url() + "/home") 75 | resp = self.br.response().read() 76 | assert self.br.viewing_html() 77 | 78 | assert 'mocktest_00_lxc' in resp 79 | assert 'Running' in resp 80 | 81 | def test_03_freeze_container(self): 82 | """ 83 | freeze the container using /action and check badge on home 84 | """ 85 | self.br.open(self.get_server_url() + "/action?action=freeze&name=mocktest_00_lxc") 86 | 87 | self.br.open(self.get_server_url() + "/home") 88 | resp = self.br.response().read() 89 | assert self.br.viewing_html() 90 | 91 | assert 'mocktest_00_lxc' in resp 92 | assert 'Frozen' in resp 93 | 94 | def test_04_unfreeze_container(self): 95 | """ 96 | unfreeze container using /action and check badge on home 97 | """ 98 | self.br.open(self.get_server_url() + "/action?action=unfreeze&name=mocktest_00_lxc") 99 | 100 | self.br.open(self.get_server_url() + "/home") 101 | resp = self.br.response().read() 102 | assert self.br.viewing_html() 103 | 104 | assert 'mocktest_00_lxc' in resp 105 | assert 'Running' in resp 106 | 107 | def test_05_stop_container(self): 108 | """ 109 | try to stop it 110 | """ 111 | self.br.open(self.get_server_url() + "/action?action=stop&name=mocktest_00_lxc") 112 | 113 | self.br.open(self.get_server_url() + "/home") 114 | resp = self.br.response().read() 115 | assert self.br.viewing_html() 116 | 117 | assert 'mocktest_00_lxc' in resp 118 | assert 'Stopped' in resp 119 | 120 | def test_06_refresh_info(self): 121 | """ 122 | the _refresh_info should return json object with host info 123 | """ 124 | self.br.open(self.get_server_url() + '/_refresh_info') 125 | 126 | j_data = self.br.response().read() 127 | assert 'cpu' in j_data 128 | assert 'disk' in j_data 129 | assert 'uptime' in j_data 130 | 131 | def test_07_create_container(self): 132 | """ 133 | try to create "test_created_container" 134 | """ 135 | self.br.open(self.get_server_url() + "/home") 136 | 137 | # select create-container form and fill it 138 | self.br.select_form(name="create_container") 139 | self.br['name'] = "test_created_container" 140 | resp = self.br.submit() 141 | 142 | assert '/home' in resp.geturl() 143 | assert 'mocktest_00_lxc' in resp.read() 144 | 145 | def test_08_create_token(self): 146 | """ 147 | try to create "test_created_container" 148 | """ 149 | self.br.open(self.get_server_url() + "/lwp/tokens") 150 | 151 | # select create-container form and fill it 152 | self.br.select_form(name="lwp_token") 153 | self.br['token'] = "mechanize_token" 154 | self.br['description'] = "my_token_desc" 155 | resp = self.br.submit() 156 | body = resp.read() 157 | 158 | assert '/lwp/tokens' in resp.geturl() 159 | assert 'mechanize_token' in body 160 | assert 'my_token_desc' in body 161 | 162 | 163 | if __name__ == '__main__': 164 | unittest.main() 165 | -------------------------------------------------------------------------------- /tests/lxc_lite.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import unittest 3 | import shutil 4 | import os 5 | 6 | import lwp.lxclite as lxc 7 | 8 | class TestLxcLite(unittest.TestCase): 9 | 10 | """ 11 | Those tests are against the lxclite class 12 | """ 13 | @classmethod 14 | def setUpClass(cls): 15 | shutil.rmtree('/tmp/lxc/', ignore_errors=True) 16 | 17 | def test_00_create(self): 18 | lxc.create('test00') 19 | assert os.path.exists('/tmp/lxc/test00') 20 | 21 | def test_01_clone(self): 22 | lxc.clone('test00', 'testclone') 23 | assert os.path.exists('/tmp/lxc/test00') 24 | assert os.path.exists('/tmp/lxc/testclone') 25 | 26 | def test_02_start(self): 27 | lxc.start('test00') 28 | 29 | def test_03_freeze(self): 30 | lxc.freeze('test00') 31 | 32 | def test_04_unfreeze(self): 33 | lxc.unfreeze('test00') 34 | 35 | def test_05_stop(self): 36 | lxc.stop('test00') 37 | 38 | def test_06_destroy(self): 39 | lxc.destroy('test00') 40 | assert not os.path.exists('/tmp/lxc/test00') 41 | 42 | if __name__ == '__main__': 43 | unittest.main() 44 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-clone: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-create used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | # lxc-clone 7 | if [[ $1 && $2 ]]; then 8 | if [[ -d /tmp/lxc/$1 ]]; then 9 | cp -r /tmp/lxc/$1 /tmp/lxc/$2 10 | exit 0 11 | fi 12 | fi 13 | 14 | exit 1 15 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-config: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-config used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 == 'lxc.lxcpath' ]]; then 7 | echo "/tmp/lxc" 8 | mkdir -p /tmp/lxc 9 | exit 0 10 | fi 11 | 12 | exit 1 13 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-create: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-create used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | # lxc-create -n 7 | if [[ $1 -eq '-n' && $2 ]]; then 8 | if [[ ! -d /tmp/lxc/$2 ]]; then 9 | mkdir -p /tmp/lxc/$2 10 | touch /tmp/lxc/$2/config 11 | echo "STOPPED" > /tmp/lxc/$2/status 12 | echo $* > /tmp/lxc/$2/params 13 | exit 0 14 | else 15 | echo "Container already exists" 16 | exit 1 17 | fi 18 | fi 19 | 20 | exit 1 21 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-destroy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-destroy used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 -eq '-n' && $2 ]]; then 7 | if [[ -d /tmp/lxc/$2 ]]; then 8 | rm -rf /tmp/lxc/$2 9 | exit 0 10 | else 11 | exit 1 12 | fi 13 | fi 14 | 15 | exit 1 16 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-freeze: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-freeze used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 -eq '-dn' && $2 ]]; then 7 | if [[ -f /tmp/lxc/$2/status ]]; then 8 | grep 'RUNNING' /tmp/lxc/$2/status > /dev/null || exit 1 # not running 9 | echo 'FROZEN' > /tmp/lxc/$2/status 10 | echo $* > /tmp/lxc/$2/params 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | exit 1 18 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-ls: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-ls used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | # lxc-ls --fancy 7 | if [[ $1 == '--fancy' ]]; then 8 | mkdir -p /tmp/lxc 9 | echo "NAME STATE IPV4 IPV6 GROUPS AUTOSTART" 10 | echo "------------------------------------------" 11 | for name in `ls /tmp/lxc`; do 12 | status=`cat /tmp/lxc/${name}/status` 13 | ip=192.168."$((RANDOM%=255))"."$((RANDOM%=255))" 14 | echo "${name} ${status} $ip - - NO" 15 | done 16 | fi 17 | 18 | exit 1 19 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-start used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 -eq '-dn' && $2 ]]; then 7 | if [[ -f /tmp/lxc/$2/status ]]; then 8 | grep 'STOPPED' /tmp/lxc/$2/status > /dev/null || exit 1 # already started 9 | echo 'RUNNING' > /tmp/lxc/$2/status 10 | echo $* > /tmp/lxc/$2/params 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | exit 1 18 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-stop: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-stop used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 -eq '-n' && $2 ]]; then 7 | if [[ -f /tmp/lxc/$2/status ]]; then 8 | grep 'RUNNING' /tmp/lxc/$2/status > /dev/null || exit 1 # already started 9 | echo 'STOPPED' > /tmp/lxc/$2/status 10 | echo $* > /tmp/lxc/$2/params 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | exit 1 18 | -------------------------------------------------------------------------------- /tests/mock-lxc/lxc-unfreeze: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # mock of the lxc-freeze used to test lxclite on travis due to: 4 | # https://github.com/travis-ci/travis-ci/issues/1273 5 | 6 | if [[ $1 -eq '-dn' && $2 ]]; then 7 | if [[ -f /tmp/lxc/$2/status ]]; then 8 | grep 'FROZEN' /tmp/lxc/$2/status > /dev/null || exit 1 # not running 9 | echo 'RUNNING' > /tmp/lxc/$2/status 10 | echo $* > /tmp/lxc/$2/params 11 | exit 0 12 | else 13 | exit 1 14 | fi 15 | fi 16 | 17 | exit 1 18 | -------------------------------------------------------------------------------- /tests/mock_lxc.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import unittest 3 | import urllib2 4 | import shutil 5 | import json 6 | import os 7 | 8 | from flask import Flask 9 | from flask.ext.testing import LiveServerTestCase 10 | 11 | from lwp.app import app 12 | from lwp.utils import connect_db 13 | 14 | 15 | class TestMockLxc(unittest.TestCase): 16 | 17 | """ 18 | Those tests are against the lxc mock system under tests/mock-lxc/ 19 | 20 | To use those tests on your machine with lxc installed you should 21 | add the path in your ENV before the default one: 22 | export PATH=`pwd`/tests/mock-lxc/:$PATH 23 | """ 24 | 25 | def test_01_config(self): 26 | shutil.rmtree('/tmp/lxc', ignore_errors=True) 27 | out = subprocess.check_output('lxc-config lxc.lxcpath', shell=True, close_fds=True).strip() 28 | assert out == '/tmp/lxc' 29 | 30 | def test_02_create(self): 31 | subprocess.check_output('lxc-create -n lxctest', shell=True, close_fds=True).strip() 32 | assert 'lxctest' in os.listdir('/tmp/lxc') 33 | with open('/tmp/lxc/lxctest/status') as f: 34 | content = f.readlines() 35 | assert 'STOPPED' in content[0].rstrip('\n') 36 | 37 | def test_03_start(self): 38 | subprocess.check_output('lxc-start -dn lxctest', shell=True, close_fds=True).strip() 39 | with open('/tmp/lxc/lxctest/status') as f: 40 | content = f.readlines() 41 | assert 'RUNNING' in content[0].rstrip('\n') 42 | 43 | def test_04_ls(self): 44 | out = subprocess.check_output('lxc-ls --fancy | grep test', shell=True, close_fds=True).strip() 45 | assert 'lxctest' in out 46 | 47 | def test_05_stop(self): 48 | subprocess.check_output('lxc-stop -n lxctest', shell=True, close_fds=True).strip() 49 | with open('/tmp/lxc/lxctest/status') as f: 50 | content = f.readlines() 51 | assert 'STOPPED' in content[0].rstrip('\n') 52 | 53 | def test_06_destroy(self): 54 | subprocess.check_output('lxc-destroy -n lxctest', shell=True, close_fds=True).strip() 55 | assert 'lxctest' not in os.listdir('/tmp/lxc') 56 | 57 | 58 | if __name__ == '__main__': 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import unittest 3 | import os 4 | 5 | 6 | class TestCmdLine(unittest.TestCase): 7 | 8 | """ 9 | Those tests are against the lwp command lines 10 | """ 11 | 12 | def test_01_generate_secret(self): 13 | assert not os.path.exists('/etc/lwp/session_secret') 14 | assert not os.path.exists('/etc/lwp/lwp.conf') 15 | subprocess.check_call('python bin/lwp --generate-session-secret', shell=True) 16 | assert os.path.exists('/etc/lwp/session_secret') 17 | 18 | def test_02_exit_if_no_config(self): 19 | assert not os.path.exists('/etc/lwp/lwp.conf') 20 | try: 21 | subprocess.check_call('python bin/lwp', shell=True) 22 | except subprocess.CalledProcessError as e: 23 | assert e.returncode 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | --------------------------------------------------------------------------------