From 6b4d62f24a47857f21a890826ee0c54b967a3c38 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 11:10:17 +0300 Subject: [PATCH 01/20] [fix] add gitignore --- .gitignore | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a295864..c3797eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,58 @@ -*.pyc -__pycache__ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit tests / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +/dist +/buid +/env +/.idea \ No newline at end of file From 40d22c470bcac313c8d374b1b54154bb87aa047d Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 11:11:44 +0300 Subject: [PATCH 02/20] [fix] add config --- lwp.conf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lwp.conf b/lwp.conf index d0a99d2..e26d413 100644 --- a/lwp.conf +++ b/lwp.conf @@ -2,12 +2,13 @@ address = 0.0.0.0 port = 5000 debug = False +secret = write_your_secret_key [database] -file = lwp.db +file = /var/lib/lwp.db [session] -time = 10 +time = 600 [overview] partition = / From 71c6483390e9a0c8649f11fac6bd886315044667 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 11:13:04 +0300 Subject: [PATCH 03/20] [fix] prepare to module structure --- lwp.py | 911 ------------------ lwp/{ => lxc}/LICENSE | 0 lwp/{ => lxc}/__init__.py | 0 {lxclite => lwp/lxclite}/LICENSE | 0 {lxclite => lwp/lxclite}/__init__.py | 0 .../static}/css/bootstrap-responsive.min.css | 0 .../static}/css/bootstrap-select.min.css | 0 {static => lwp/static}/css/bootstrap.css | 0 .../static}/css/bootstrapSwitch.css | 0 {static => lwp/static}/ico/favicon.ico | Bin .../img/glyphicons-halflings-white.png | Bin .../static}/img/glyphicons-halflings.png | Bin {static => lwp/static}/js/bootstrap.min.js | 0 {static => lwp/static}/js/bootstrapSwitch.js | 0 {static => lwp/static}/js/html5slider.js | 0 .../static}/js/jqBootstrapValidation.js | 0 {templates => lwp/templates}/about.html | 0 {templates => lwp/templates}/checkconfig.html | 0 {templates => lwp/templates}/edit.html | 0 .../templates}/includes/aside.html | 0 .../templates}/includes/modal_clone.html | 0 .../templates}/includes/modal_create.html | 0 .../templates}/includes/modal_delete.html | 0 .../templates}/includes/modal_destroy.html | 0 .../templates}/includes/modal_new_user.html | 0 .../templates}/includes/modal_reboot.html | 0 .../templates}/includes/modals.html | 0 .../templates}/includes/nav.html | 0 {templates => lwp/templates}/index.html | 0 {templates => lwp/templates}/layout.html | 0 {templates => lwp/templates}/login.html | 0 {templates => lwp/templates}/lxc-net.html | 0 {templates => lwp/templates}/users.html | 0 33 files changed, 911 deletions(-) delete mode 100644 lwp.py rename lwp/{ => lxc}/LICENSE (100%) rename lwp/{ => lxc}/__init__.py (100%) rename {lxclite => lwp/lxclite}/LICENSE (100%) rename {lxclite => lwp/lxclite}/__init__.py (100%) rename {static => lwp/static}/css/bootstrap-responsive.min.css (100%) rename {static => lwp/static}/css/bootstrap-select.min.css (100%) rename {static => lwp/static}/css/bootstrap.css (100%) rename {static => lwp/static}/css/bootstrapSwitch.css (100%) rename {static => lwp/static}/ico/favicon.ico (100%) rename {static => lwp/static}/img/glyphicons-halflings-white.png (100%) rename {static => lwp/static}/img/glyphicons-halflings.png (100%) rename {static => lwp/static}/js/bootstrap.min.js (100%) rename {static => lwp/static}/js/bootstrapSwitch.js (100%) rename {static => lwp/static}/js/html5slider.js (100%) rename {static => lwp/static}/js/jqBootstrapValidation.js (100%) rename {templates => lwp/templates}/about.html (100%) rename {templates => lwp/templates}/checkconfig.html (100%) rename {templates => lwp/templates}/edit.html (100%) rename {templates => lwp/templates}/includes/aside.html (100%) rename {templates => lwp/templates}/includes/modal_clone.html (100%) rename {templates => lwp/templates}/includes/modal_create.html (100%) rename {templates => lwp/templates}/includes/modal_delete.html (100%) rename {templates => lwp/templates}/includes/modal_destroy.html (100%) rename {templates => lwp/templates}/includes/modal_new_user.html (100%) rename {templates => lwp/templates}/includes/modal_reboot.html (100%) rename {templates => lwp/templates}/includes/modals.html (100%) rename {templates => lwp/templates}/includes/nav.html (100%) rename {templates => lwp/templates}/index.html (100%) rename {templates => lwp/templates}/layout.html (100%) rename {templates => lwp/templates}/login.html (100%) rename {templates => lwp/templates}/lxc-net.html (100%) rename {templates => lwp/templates}/users.html (100%) diff --git a/lwp.py b/lwp.py deleted file mode 100644 index 1684614..0000000 --- a/lwp.py +++ /dev/null @@ -1,911 +0,0 @@ -# LXC Python Library -# for compatibility with LXC 0.8 and 0.9 -# on Ubuntu 12.04/12.10/13.04 - -# Author: Elie Deloumeau -# Contact: elie@deloumeau.fr - -# The MIT License (MIT) -# Copyright (c) 2013 Elie Deloumeau - -# 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: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# 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. - -import lxclite as lxc -import lwp -import subprocess -import time -import re -import hashlib -import sqlite3 -import os - -from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash, jsonify - -try: - import configparser -except ImportError: - import ConfigParser as configparser - -# configuration -config = configparser.SafeConfigParser() -config.readfp(open('lwp.conf')) - -SECRET_KEY = '\xb13\xb6\xfb+Z\xe8\xd1n\x80\x9c\xe7KM' \ - '\x1c\xc1\xa7\xf8\xbeY\x9a\xfa<.' - -DEBUG = config.getboolean('global', 'debug') -DATABASE = config.get('database', 'file') -ADDRESS = config.get('global', 'address') -PORT = int(config.get('global', 'port')) - - -# Flask app -app = Flask(__name__) -app.config.from_object(__name__) - - -def connect_db(): - ''' - SQLite3 connect function - ''' - - return sqlite3.connect(app.config['DATABASE']) - - -@app.before_request -def before_request(): - ''' - executes functions before all requests - ''' - - check_session_limit() - g.db = connect_db() - - -@app.teardown_request -def teardown_request(exception): - ''' - executes functions after all requests - ''' - - if hasattr(g, 'db'): - g.db.close() - - -@app.route('/') -@app.route('/home') -def home(): - ''' - home page function - ''' - - if 'logged_in' in session: - listx = lxc.listx() - containers_all = [] - - for status in ['RUNNING', 'FROZEN', 'STOPPED']: - containers_by_status = [] - - for container in listx[status]: - containers_by_status.append({ - 'name': container, - 'memusg': lwp.memory_usage(container), - 'settings': lwp.get_container_settings(container) - }) - containers_all.append({ - 'status': status.lower(), - 'containers': containers_by_status - }) - - return render_template('index.html', containers=lxc.ls(), - containers_all=containers_all, - dist=lwp.check_ubuntu(), - templates=lwp.get_templates_list()) - return render_template('login.html') - - -@app.route('/about') -def about(): - ''' - about page - ''' - - if 'logged_in' in session: - return render_template('about.html', containers=lxc.ls(), - version=lwp.check_version()) - return render_template('login.html') - - -@app.route('//edit', methods=['POST', 'GET']) -def edit(container=None): - ''' - edit containers page and actions if form post request - ''' - - if 'logged_in' in session: - host_memory = lwp.host_memory_usage() - if request.method == 'POST': - cfg = lwp.get_container_settings(container) - 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]?)(/(3[0-2]|[12]?[0-9]))?' - info = lxc.info(container) - - form = {} - form['type'] = request.form['type'] - form['link'] = request.form['link'] - try: - form['flags'] = request.form['flags'] - except KeyError: - form['flags'] = 'down' - form['hwaddr'] = request.form['hwaddress'] - form['rootfs'] = request.form['rootfs'] - form['utsname'] = request.form['hostname'] - form['ipv4'] = request.form['ipaddress'] - form['memlimit'] = request.form['memlimit'] - form['swlimit'] = request.form['swlimit'] - form['cpus'] = request.form['cpus'] - form['shares'] = request.form['cpushares'] - try: - form['autostart'] = request.form['autostart'] - except KeyError: - form['autostart'] = False - - if form['utsname'] != cfg['utsname'] and \ - re.match('(?!^containers$)|^(([a-zA-Z0-9]|[a-zA-Z0-9]' - '[a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|' - '[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$', - form['utsname']): - lwp.push_config_value('lxc.utsname', form['utsname'], - container=container) - flash(u'Hostname updated for %s!' % container, 'success') - - if form['flags'] != cfg['flags'] and \ - re.match('^(up|down)$', form['flags']): - lwp.push_config_value('lxc.network.flags', form['flags'], - container=container) - flash(u'Network flag updated for %s!' % container, 'success') - - if form['type'] != cfg['type'] and \ - re.match('^\w+$', form['type']): - lwp.push_config_value('lxc.network.type', form['type'], - container=container) - flash(u'Link type updated for %s!' % container, 'success') - - if form['link'] != cfg['link'] and \ - re.match('^[a-zA-Z0-9_-]+$', form['link']): - lwp.push_config_value('lxc.network.link', form['link'], - container=container) - flash(u'Link name updated for %s!' % container, 'success') - - if form['hwaddr'] != cfg['hwaddr'] and \ - re.match('^([a-fA-F0-9]{2}[:|\-]?){6}$', form['hwaddr']): - lwp.push_config_value('lxc.network.hwaddr', form['hwaddr'], - container=container) - flash(u'Hardware address updated for %s!' % container, - 'success') - - if (not form['ipv4'] and form['ipv4'] != cfg['ipv4']) or \ - (form['ipv4'] != cfg['ipv4'] and - re.match('^%s$' % ip_regex, form['ipv4'])): - lwp.push_config_value('lxc.network.ipv4', form['ipv4'], - container=container) - flash(u'IP address updated for %s!' % container, 'success') - - if form['memlimit'] != cfg['memlimit'] and \ - form['memlimit'].isdigit() and \ - int(form['memlimit']) <= int(host_memory['total']): - if int(form['memlimit']) == int(host_memory['total']): - form['memlimit'] = '' - - if form['memlimit'] != cfg['memlimit']: - lwp.push_config_value('lxc.cgroup.memory.limit_in_bytes', - form['memlimit'], - container=container) - if info["state"].lower() != 'stopped': - lxc.cgroup(container, - 'lxc.cgroup.memory.limit_in_bytes', - form['memlimit']) - flash(u'Memory limit updated for %s!' % container, - 'success') - - if form['swlimit'] != cfg['swlimit'] and \ - form['swlimit'].isdigit() and \ - int(form['swlimit']) <= int(host_memory['total'] * 2): - if int(form['swlimit']) == int(host_memory['total'] * 2): - form['swlimit'] = '' - - if form['swlimit'].isdigit(): - form['swlimit'] = int(form['swlimit']) - - if form['memlimit'].isdigit(): - form['memlimit'] = int(form['memlimit']) - - if (form['memlimit'] == '' and form['swlimit'] != '') or \ - (form['memlimit'] > form['swlimit'] and - form['swlimit'] != ''): - flash(u'Can\'t assign swap memory lower than' - ' the memory limit', 'warning') - - elif form['swlimit'] != cfg['swlimit'] and \ - form['memlimit'] <= form['swlimit']: - lwp.push_config_value( - 'lxc.cgroup.memory.memsw.limit_in_bytes', - form['swlimit'], container=container) - - if info["state"].lower() != 'stopped': - lxc.cgroup(container, - 'lxc.cgroup.memory.memsw.limit_in_bytes', - form['swlimit']) - flash(u'Swap limit updated for %s!' % container, 'success') - - if (not form['cpus'] and form['cpus'] != cfg['cpus']) or \ - (form['cpus'] != cfg['cpus'] and - re.match('^[0-9,-]+$', form['cpus'])): - lwp.push_config_value('lxc.cgroup.cpuset.cpus', form['cpus'], - container=container) - - if info["state"].lower() != 'stopped': - lxc.cgroup(container, 'lxc.cgroup.cpuset.cpus', - form['cpus']) - flash(u'CPUs updated for %s!' % container, 'success') - - if (not form['shares'] and form['shares'] != cfg['shares']) or \ - (form['shares'] != cfg['shares'] and - re.match('^[0-9]+$', form['shares'])): - lwp.push_config_value('lxc.cgroup.cpu.shares', form['shares'], - container=container) - if info["state"].lower() != 'stopped': - lxc.cgroup(container, 'lxc.cgroup.cpu.shares', - form['shares']) - flash(u'CPU shares updated for %s!' % container, 'success') - - if form['rootfs'] != cfg['rootfs'] and \ - re.match('^[a-zA-Z0-9_/\-\.]+', form['rootfs']): - lwp.push_config_value('lxc.rootfs', form['rootfs'], - container=container) - flash(u'Rootfs updated!' % container, 'success') - - auto = lwp.ls_auto() - if form['autostart'] == 'True' and \ - not ('%s.conf' % container) in auto: - try: - os.symlink('/var/lib/lxc/%s/config' % container, - '/etc/lxc/auto/%s.conf' % container) - flash(u'Autostart enabled for %s' % container, 'success') - except OSError: - flash(u'Unable to create symlink \'/etc/lxc/auto/%s.conf\'' - % container, 'error') - elif not form['autostart'] and ('%s.conf' % container) in auto: - try: - os.remove('/etc/lxc/auto/%s.conf' % container) - flash(u'Autostart disabled for %s' % container, 'success') - except OSError: - flash(u'Unable to remove symlink', 'error') - - info = lxc.info(container) - status = info['state'] - pid = info['pid'] - - infos = {'status': status, - 'pid': pid, - 'memusg': lwp.memory_usage(container)} - return render_template('edit.html', containers=lxc.ls(), - container=container, infos=infos, - settings=lwp.get_container_settings(container), - host_memory=host_memory) - return render_template('login.html') - - -@app.route('/settings/lxc-net', methods=['POST', 'GET']) -def lxc_net(): - ''' - lxc-net (/etc/default/lxc) settings page and actions if form post request - ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - if request.method == 'POST': - if lxc.running() == []: - cfg = lwp.get_net_settings() - 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]?)' - - form = {} - try: - form['use'] = request.form['use'] - except KeyError: - form['use'] = 'false' - - try: - form['bridge'] = request.form['bridge'] - except KeyError: - form['bridge'] = None - - try: - form['address'] = request.form['address'] - except KeyError: - form['address'] = None - - try: - form['netmask'] = request.form['netmask'] - except KeyError: - form['netmask'] = None - - try: - form['network'] = request.form['network'] - except KeyError: - form['network'] = None - - try: - form['range'] = request.form['range'] - except KeyError: - form['range'] = None - - try: - form['max'] = request.form['max'] - except KeyError: - form['max'] = None - - if form['use'] == 'true' and form['use'] != cfg['use']: - lwp.push_net_value('USE_LXC_BRIDGE', 'true') - - elif form['use'] == 'false' and form['use'] != cfg['use']: - lwp.push_net_value('USE_LXC_BRIDGE', 'false') - - if form['bridge'] and form['bridge'] != cfg['bridge'] \ - and re.match('^[a-zA-Z0-9_-]+$', form['bridge']): - lwp.push_net_value('LXC_BRIDGE', form['bridge']) - - if form['address'] and form['address'] != cfg['address'] \ - and re.match('^%s$' % ip_regex, form['address']): - lwp.push_net_value('LXC_ADDR', form['address']) - - if form['netmask'] and form['netmask'] != cfg['netmask'] \ - and re.match('^%s$' % ip_regex, form['netmask']): - lwp.push_net_value('LXC_NETMASK', form['netmask']) - - if form['network'] and form['network'] != cfg['network'] and \ - re.match('^%s(?:/\d{1,2}|)$' % ip_regex, - form['network']): - lwp.push_net_value('LXC_NETWORK', form['network']) - - if form['range'] and form['range'] != cfg['range'] and \ - re.match('^%s,%s$' % (ip_regex, ip_regex), - form['range']): - lwp.push_net_value('LXC_DHCP_RANGE', form['range']) - - if form['max'] and form['max'] != cfg['max'] and \ - re.match('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', - form['max']): - lwp.push_net_value('LXC_DHCP_MAX', form['max']) - - if lwp.net_restart() == 0: - flash(u'LXC Network settings applied successfully!', - 'success') - else: - flash(u'Failed to restart LXC networking.', 'error') - else: - flash(u'Stop all containers before restart lxc-net.', - 'warning') - return render_template('lxc-net.html', containers=lxc.ls(), - cfg=lwp.get_net_settings(), - running=lxc.running()) - return render_template('login.html') - - -@app.route('/lwp/users', methods=['POST', 'GET']) -def lwp_users(): - ''' - returns users and get posts request : can edit or add user in page. - this funtction uses sqlite3 - ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - try: - trash = request.args.get('trash') - except KeyError: - trash = 0 - - su_users = query_db("SELECT COUNT(id) as num FROM users " - "WHERE su='Yes'", [], one=True) - - if request.args.get('token') == session.get('token') and \ - int(trash) == 1 and request.args.get('userid') and \ - request.args.get('username'): - nb_users = query_db("SELECT COUNT(id) as num FROM users", [], - one=True) - - if nb_users['num'] > 1: - if su_users['num'] <= 1: - su_user = query_db("SELECT username FROM users " - "WHERE su='Yes'", [], one=True) - - if su_user['username'] == request.args.get('username'): - flash(u'Can\'t delete the last admin user : %s' % - request.args.get('username'), 'error') - return redirect(url_for('lwp_users')) - - g.db.execute("DELETE FROM users WHERE id=? AND username=?", - [request.args.get('userid'), - request.args.get('username')]) - g.db.commit() - flash(u'Deleted %s' % request.args.get('username'), 'success') - return redirect(url_for('lwp_users')) - - flash(u'Can\'t delete the last user!', 'error') - return redirect(url_for('lwp_users')) - - if request.method == 'POST': - users = query_db('SELECT id, name, username, su FROM users ' - 'ORDER BY id ASC') - - if request.form['newUser'] == 'True': - if not request.form['username'] in \ - [user['username'] for user in users]: - if re.match('^\w+$', request.form['username']) and \ - request.form['password1']: - if request.form['password1'] == \ - request.form['password2']: - if request.form['name']: - if re.match('[a-z A-Z0-9]{3,32}', - request.form['name']): - g.db.execute( - "INSERT INTO users " - "(name, username, password) " - "VALUES (?, ?, ?)", - [request.form['name'], - request.form['username'], - hash_passwd( - request.form['password1'])]) - g.db.commit() - else: - flash(u'Invalid name!', 'error') - else: - g.db.execute("INSERT INTO users " - "(username, password) VALUES " - "(?, ?)", - [request.form['username'], - hash_passwd( - request.form['password1'])]) - g.db.commit() - - flash(u'Created %s' % request.form['username'], - 'success') - else: - flash(u'No password match', 'error') - else: - flash(u'Invalid username or password!', 'error') - else: - flash(u'Username already exist!', 'error') - - elif request.form['newUser'] == 'False': - if request.form['password1'] == request.form['password2']: - if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): - if su_users['num'] <= 1: - su = 'Yes' - else: - try: - su = request.form['su'] - except KeyError: - su = 'No' - - if not request.form['name']: - g.db.execute("UPDATE users SET name='', su=? " - "WHERE username=?", - [su, request.form['username']]) - g.db.commit() - elif request.form['name'] and \ - not request.form['password1'] and \ - not request.form['password2']: - g.db.execute("UPDATE users SET name=?, su=? " - "WHERE username=?", - [request.form['name'], su, - request.form['username']]) - g.db.commit() - elif request.form['name'] and \ - request.form['password1'] and \ - request.form['password2']: - g.db.execute("UPDATE users SET " - "name=?, password=?, su=? WHERE " - "username=?", - [request.form['name'], - hash_passwd( - request.form['password1']), - su, request.form['username']]) - g.db.commit() - elif request.form['password1'] and \ - request.form['password2']: - g.db.execute("UPDATE users SET password=?, su=? " - "WHERE username=?", - [hash_passwd( - request.form['password1']), - su, request.form['username']]) - g.db.commit() - - flash(u'Updated', 'success') - else: - flash(u'Invalid name!', 'error') - else: - flash(u'No password match', 'error') - else: - flash(u'Unknown error!', 'error') - - users = query_db("SELECT id, name, username, su FROM users " - "ORDER BY id ASC") - nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) - su_users = query_db("SELECT COUNT(id) as num FROM users " - "WHERE su='Yes'", [], one=True) - - return render_template('users.html', containers=lxc.ls(), users=users, - nb_users=nb_users, su_users=su_users) - return render_template('login.html') - - -@app.route('/checkconfig') -def checkconfig(): - ''' - returns the display of lxc-checkconfig command - ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - return render_template('checkconfig.html', containers=lxc.ls(), - cfg=lxc.checkconfig()) - return render_template('login.html') - - -@app.route('/action', methods=['GET']) -def action(): - ''' - manage all actions related to containers - lxc-start, lxc-stop, etc... - ''' - if 'logged_in' in session: - if request.args['token'] == session.get('token'): - action = request.args['action'] - name = request.args['name'] - - if action == 'start': - try: - if lxc.start(name) == 0: - # Fix bug : "the container is randomly not - # displayed in overview list after a boot" - time.sleep(1) - flash(u'Container %s started successfully!' % name, - 'success') - else: - flash(u'Unable to start %s!' % name, 'error') - except lxc.ContainerAlreadyRunning: - flash(u'Container %s is already running!' % name, 'error') - elif action == 'stop': - try: - if lxc.stop(name) == 0: - flash(u'Container %s stopped successfully!' % name, - 'success') - else: - flash(u'Unable to stop %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s is already stopped!' % name, 'error') - elif action == 'freeze': - try: - if lxc.freeze(name) == 0: - flash(u'Container %s frozen successfully!' % name, - 'success') - else: - flash(u'Unable to freeze %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s not running!' % name, 'error') - elif action == 'unfreeze': - try: - if lxc.unfreeze(name) == 0: - flash(u'Container %s unfrozen successfully!' % name, - 'success') - else: - flash(u'Unable to unfeeze %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s not frozen!' % name, 'error') - elif action == 'destroy': - if session['su'] != 'Yes': - return abort(403) - try: - if lxc.destroy(name) == 0: - flash(u'Container %s destroyed successfully!' % name, - 'success') - else: - flash(u'Unable to destroy %s!' % name, 'error') - except lxc.ContainerDoesntExists: - flash(u'The Container %s does not exists!' % name, 'error') - elif action == 'reboot' and name == 'host': - if session['su'] != 'Yes': - return abort(403) - msg = '\v*** LXC Web Panel *** \ - \nReboot from web panel' - try: - subprocess.check_call('/sbin/shutdown -r now \'%s\'' % msg, - shell=True) - flash(u'System will now restart!', 'success') - except: - flash(u'System error!', 'error') - try: - if request.args['from'] == 'edit': - return redirect('../%s/edit' % name) - else: - return redirect(url_for('home')) - except: - return redirect(url_for('home')) - return render_template('login.html') - - -@app.route('/action/create-container', methods=['GET', 'POST']) -def create_container(): - ''' - verify all forms to create a container - ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - if request.method == 'POST': - name = request.form['name'] - template = request.form['template'] - command = request.form['command'] - - if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): - storage_method = request.form['backingstore'] - - if storage_method == 'default': - try: - if lxc.create(name, template=template, - xargs=command) == 0: - flash(u'Container %s created successfully!' % name, - 'success') - else: - flash(u'Failed to create %s!' % name, 'error') - except lxc.ContainerAlreadyExists: - flash(u'The Container %s is already created!' % name, - 'error') - except subprocess.CalledProcessError: - flash(u'Error!' % name, 'error') - - elif storage_method == 'directory': - directory = request.form['dir'] - - if re.match('^/[a-zA-Z0-9_/-]+$', directory) and \ - directory != '': - try: - if lxc.create(name, template=template, - storage='dir --dir %s' % directory, - xargs=command) == 0: - flash(u'Container %s created successfully!' - % name, 'success') - else: - flash(u'Failed to create %s!' % name, 'error') - except lxc.ContainerAlreadyExists: - flash(u'The Container %s is already created!' - % name, 'error') - except subprocess.CalledProcessError: - flash(u'Error!' % name, 'error') - - elif storage_method == 'lvm': - lvname = request.form['lvname'] - vgname = request.form['vgname'] - fstype = request.form['fstype'] - fssize = request.form['fssize'] - storage_options = 'lvm' - - if re.match('^[a-zA-Z0-9_-]+$', lvname) and lvname != '': - storage_options += ' --lvname %s' % lvname - if re.match('^[a-zA-Z0-9_-]+$', vgname) and vgname != '': - storage_options += ' --vgname %s' % vgname - if re.match('^[a-z0-9]+$', fstype) and fstype != '': - storage_options += ' --fstype %s' % fstype - if re.match('^[0-9][G|M]$', fssize) and fssize != '': - storage_options += ' --fssize %s' % fssize - - try: - if lxc.create(name, template=template, - storage=storage_options, - xargs=command) == 0: - flash(u'Container %s created successfully!' % name, - 'success') - else: - flash(u'Failed to create %s!' % name, 'error') - except lxc.ContainerAlreadyExists: - flash(u'The container/logical volume %s is ' - 'already created!' % name, 'error') - except subprocess.CalledProcessError: - flash(u'Error!' % name, 'error') - - else: - flash(u'Missing parameters to create container!', 'error') - - else: - if name == '': - flash(u'Please enter a container name!', 'error') - else: - flash(u'Invalid name for \"%s\"!' % name, 'error') - - return redirect(url_for('home')) - return render_template('login.html') - - -@app.route('/action/clone-container', methods=['GET', 'POST']) -def clone_container(): - ''' - verify all forms to clone a container - ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - if request.method == 'POST': - orig = request.form['orig'] - name = request.form['name'] - - try: - snapshot = request.form['snapshot'] - if snapshot == 'True': - snapshot = True - except KeyError: - snapshot = False - - if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): - out = None - - try: - out = lxc.clone(orig=orig, new=name, snapshot=snapshot) - except lxc.ContainerAlreadyExists: - flash(u'The Container %s already exists!' % name, 'error') - except subprocess.CalledProcessError: - flash(u'Can\'t snapshot a directory', 'error') - - if out and out == 0: - flash(u'Container %s cloned into %s successfully!' - % (orig, name), 'success') - elif out and out != 0: - flash(u'Failed to clone %s into %s!' % (orig, name), - 'error') - - else: - if name == '': - flash(u'Please enter a container name!', 'error') - else: - flash(u'Invalid name for \"%s\"!' % name, 'error') - - return redirect(url_for('home')) - return render_template('login.html') - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - if request.method == 'POST': - request_username = request.form['username'] - request_passwd = hash_passwd(request.form['password']) - - current_url = request.form['url'] - - user = query_db('select name, username, su from users where username=?' - 'and password=?', [request_username, request_passwd], - one=True) - - if user: - session['logged_in'] = True - session['token'] = get_token() - session['last_activity'] = int(time.time()) - session['username'] = user['username'] - session['name'] = user['name'] - session['su'] = user['su'] - flash(u'You are logged in!', 'success') - - if current_url == url_for('login'): - return redirect(url_for('home')) - return redirect(current_url) - - flash(u'Invalid username or password!', 'error') - return render_template('login.html') - - -@app.route('/logout') -def logout(): - session.pop('logged_in', None) - session.pop('token', None) - session.pop('last_activity', None) - session.pop('username', None) - session.pop('name', None) - session.pop('su', None) - flash(u'You are logged out!', 'success') - return redirect(url_for('login')) - - -@app.route('/_refresh_cpu_host') -def refresh_cpu_host(): - if 'logged_in' in session: - return lwp.host_cpu_percent() - - -@app.route('/_refresh_uptime_host') -def refresh_uptime_host(): - if 'logged_in' in session: - return jsonify(lwp.host_uptime()) - - -@app.route('/_refresh_disk_host') -def refresh_disk_host(): - if 'logged_in' in session: - return jsonify(lwp.host_disk_usage(partition=config.get('overview', - 'partition'))) - - -@app.route('/_refresh_memory_') -def refresh_memory_containers(name=None): - if 'logged_in' in session: - if name == 'containers': - containers_running = lxc.running() - containers = [] - for container in containers_running: - container = container.replace(' (auto)', '') - containers.append({'name': container, - 'memusg': lwp.memory_usage(container)}) - return jsonify(data=containers) - elif name == 'host': - return jsonify(lwp.host_memory_usage()) - return jsonify({'memusg': lwp.memory_usage(name)}) - - -@app.route('/_check_version') -def check_version(): - if 'logged_in' in session: - return jsonify(lwp.check_version()) - - -def hash_passwd(passwd): - return hashlib.sha512(passwd.encode()).hexdigest() - - -def get_token(): - return hashlib.md5(str(time.time()).encode()).hexdigest() - - -def query_db(query, args=(), one=False): - cur = g.db.execute(query, args) - rv = [dict((cur.description[idx][0], value) - for idx, value in enumerate(row)) for row in cur.fetchall()] - return (rv[0] if rv else None) if one else rv - - -def check_session_limit(): - if 'logged_in' in session and session.get('last_activity') is not None: - now = int(time.time()) - limit = now - 60 * int(config.get('session', 'time')) - last_activity = session.get('last_activity') - if last_activity < limit: - flash(u'Session timed out !', 'info') - logout() - else: - session['last_activity'] = now - -if __name__ == '__main__': - app.run(host=app.config['ADDRESS'], port=app.config['PORT']) diff --git a/lwp/LICENSE b/lwp/lxc/LICENSE similarity index 100% rename from lwp/LICENSE rename to lwp/lxc/LICENSE diff --git a/lwp/__init__.py b/lwp/lxc/__init__.py similarity index 100% rename from lwp/__init__.py rename to lwp/lxc/__init__.py diff --git a/lxclite/LICENSE b/lwp/lxclite/LICENSE similarity index 100% rename from lxclite/LICENSE rename to lwp/lxclite/LICENSE diff --git a/lxclite/__init__.py b/lwp/lxclite/__init__.py similarity index 100% rename from lxclite/__init__.py rename to lwp/lxclite/__init__.py diff --git a/static/css/bootstrap-responsive.min.css b/lwp/static/css/bootstrap-responsive.min.css similarity index 100% rename from static/css/bootstrap-responsive.min.css rename to lwp/static/css/bootstrap-responsive.min.css diff --git a/static/css/bootstrap-select.min.css b/lwp/static/css/bootstrap-select.min.css similarity index 100% rename from static/css/bootstrap-select.min.css rename to lwp/static/css/bootstrap-select.min.css diff --git a/static/css/bootstrap.css b/lwp/static/css/bootstrap.css similarity index 100% rename from static/css/bootstrap.css rename to lwp/static/css/bootstrap.css diff --git a/static/css/bootstrapSwitch.css b/lwp/static/css/bootstrapSwitch.css similarity index 100% rename from static/css/bootstrapSwitch.css rename to lwp/static/css/bootstrapSwitch.css diff --git a/static/ico/favicon.ico b/lwp/static/ico/favicon.ico similarity index 100% rename from static/ico/favicon.ico rename to lwp/static/ico/favicon.ico diff --git a/static/img/glyphicons-halflings-white.png b/lwp/static/img/glyphicons-halflings-white.png similarity index 100% rename from static/img/glyphicons-halflings-white.png rename to lwp/static/img/glyphicons-halflings-white.png diff --git a/static/img/glyphicons-halflings.png b/lwp/static/img/glyphicons-halflings.png similarity index 100% rename from static/img/glyphicons-halflings.png rename to lwp/static/img/glyphicons-halflings.png diff --git a/static/js/bootstrap.min.js b/lwp/static/js/bootstrap.min.js similarity index 100% rename from static/js/bootstrap.min.js rename to lwp/static/js/bootstrap.min.js diff --git a/static/js/bootstrapSwitch.js b/lwp/static/js/bootstrapSwitch.js similarity index 100% rename from static/js/bootstrapSwitch.js rename to lwp/static/js/bootstrapSwitch.js diff --git a/static/js/html5slider.js b/lwp/static/js/html5slider.js similarity index 100% rename from static/js/html5slider.js rename to lwp/static/js/html5slider.js diff --git a/static/js/jqBootstrapValidation.js b/lwp/static/js/jqBootstrapValidation.js similarity index 100% rename from static/js/jqBootstrapValidation.js rename to lwp/static/js/jqBootstrapValidation.js diff --git a/templates/about.html b/lwp/templates/about.html similarity index 100% rename from templates/about.html rename to lwp/templates/about.html diff --git a/templates/checkconfig.html b/lwp/templates/checkconfig.html similarity index 100% rename from templates/checkconfig.html rename to lwp/templates/checkconfig.html diff --git a/templates/edit.html b/lwp/templates/edit.html similarity index 100% rename from templates/edit.html rename to lwp/templates/edit.html diff --git a/templates/includes/aside.html b/lwp/templates/includes/aside.html similarity index 100% rename from templates/includes/aside.html rename to lwp/templates/includes/aside.html diff --git a/templates/includes/modal_clone.html b/lwp/templates/includes/modal_clone.html similarity index 100% rename from templates/includes/modal_clone.html rename to lwp/templates/includes/modal_clone.html diff --git a/templates/includes/modal_create.html b/lwp/templates/includes/modal_create.html similarity index 100% rename from templates/includes/modal_create.html rename to lwp/templates/includes/modal_create.html diff --git a/templates/includes/modal_delete.html b/lwp/templates/includes/modal_delete.html similarity index 100% rename from templates/includes/modal_delete.html rename to lwp/templates/includes/modal_delete.html diff --git a/templates/includes/modal_destroy.html b/lwp/templates/includes/modal_destroy.html similarity index 100% rename from templates/includes/modal_destroy.html rename to lwp/templates/includes/modal_destroy.html diff --git a/templates/includes/modal_new_user.html b/lwp/templates/includes/modal_new_user.html similarity index 100% rename from templates/includes/modal_new_user.html rename to lwp/templates/includes/modal_new_user.html diff --git a/templates/includes/modal_reboot.html b/lwp/templates/includes/modal_reboot.html similarity index 100% rename from templates/includes/modal_reboot.html rename to lwp/templates/includes/modal_reboot.html diff --git a/templates/includes/modals.html b/lwp/templates/includes/modals.html similarity index 100% rename from templates/includes/modals.html rename to lwp/templates/includes/modals.html diff --git a/templates/includes/nav.html b/lwp/templates/includes/nav.html similarity index 100% rename from templates/includes/nav.html rename to lwp/templates/includes/nav.html diff --git a/templates/index.html b/lwp/templates/index.html similarity index 100% rename from templates/index.html rename to lwp/templates/index.html diff --git a/templates/layout.html b/lwp/templates/layout.html similarity index 100% rename from templates/layout.html rename to lwp/templates/layout.html diff --git a/templates/login.html b/lwp/templates/login.html similarity index 100% rename from templates/login.html rename to lwp/templates/login.html diff --git a/templates/lxc-net.html b/lwp/templates/lxc-net.html similarity index 100% rename from templates/lxc-net.html rename to lwp/templates/lxc-net.html diff --git a/templates/users.html b/lwp/templates/users.html similarity index 100% rename from templates/users.html rename to lwp/templates/users.html From 580da60a0bc8a626627682218926be86d92d63f3 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 12:30:43 +0300 Subject: [PATCH 04/20] Revert "[fix] detect only containers exclude another like lost+found" This reverts commit ff1132923ff301c5ff0a7d0158c6ad77b5abb9e3. --- lwp/lxclite/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index 2dc6cc0..e2e15bb 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -151,7 +151,7 @@ def ls(): try: ct_list = [x for x in os.listdir(base_path) - if os.path.isdir(os.path.join(base_path, x)) and os.path.exists(os.path.join(base_path, x, 'config'))] + if os.path.isdir(os.path.join(base_path, x))] except OSError: ct_list = [] From 57455a1c113908afefaa67fe2f4651c1f6de5b4b Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 16:42:59 +0300 Subject: [PATCH 05/20] [fix] config don't need. You may use --gen-conf or/and --config --- lwp.conf | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 lwp.conf diff --git a/lwp.conf b/lwp.conf deleted file mode 100644 index e26d413..0000000 --- a/lwp.conf +++ /dev/null @@ -1,14 +0,0 @@ -[global] -address = 0.0.0.0 -port = 5000 -debug = False -secret = write_your_secret_key - -[database] -file = /var/lib/lwp.db - -[session] -time = 600 - -[overview] -partition = / From c43f68c33b48136386be590b7e668ea34620b4a9 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 16:43:33 +0300 Subject: [PATCH 06/20] [fix] relative import --- lwp/lxc/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lwp/lxc/__init__.py b/lwp/lxc/__init__.py index 9ba9a76..0acfaba 100644 --- a/lwp/lxc/__init__.py +++ b/lwp/lxc/__init__.py @@ -27,8 +27,7 @@ # THE SOFTWARE. import sys -sys.path.append('../') -from lxclite import exists, stopped, ContainerDoesntExists +from ..lxclite import exists, stopped, ContainerDoesntExists import os import platform From 04d1d86963c23803ab63f8409289fef0ad3e831d Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 16:43:52 +0300 Subject: [PATCH 07/20] [fix] little fixes --- lwp/lxc/__init__.py | 7 ++----- lwp/lxclite/__init__.py | 15 +++++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/lwp/lxc/__init__.py b/lwp/lxc/__init__.py index 0acfaba..8218c2a 100644 --- a/lwp/lxc/__init__.py +++ b/lwp/lxc/__init__.py @@ -185,7 +185,7 @@ def host_cpu_percent(): return str('%.1f' % percent) -def host_disk_usage(partition=None): +def host_disk_usage(directory='/var/lib/lxc'): ''' returns a dict of disk usage values {'total': usage[1], @@ -193,10 +193,7 @@ def host_disk_usage(partition=None): 'free': usage[3], 'percent': usage[4]} ''' - if not partition: - partition = '/' - - usage = subprocess.check_output(['df -h %s' % partition], + usage = subprocess.check_output(['df -h "%s"' % directory], universal_newlines=True, shell=True).split('\n')[1].split() return {'total': usage[1], diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index e2e15bb..1e01f50 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -29,7 +29,7 @@ import subprocess import os - +# TODO: Use it everywhere. def _run(cmd, output=False): ''' To run command easier @@ -44,8 +44,11 @@ def _run(cmd, output=False): return out - return subprocess.check_call('{}'.format(cmd), shell=True, - universal_newlines=True) # returns 0 for True + try: + subprocess.check_call('{}'.format(cmd), shell=True, universal_newlines=True) # returns 0 for True + return True + except subprocess.CalledProcessError: + return False class ContainerAlreadyExists(Exception): @@ -124,7 +127,11 @@ def info(container): 'Container {} does not exist!'.format(container)) output = _run('lxc-info -qn {}|grep -i "State\|PID"'.format(container), - output=True).splitlines() + output=True) + if output: + output = output.splitlines() + else: + return {"state": None, "pid": None} state = output[0].split()[1] From 4087b1cfea7635d5d4903e78cf69a7f4a11f5e82 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Mon, 1 Dec 2014 16:44:15 +0300 Subject: [PATCH 08/20] [fix] module structure --- MANIFEST.in | 3 + lwp/__init__.py | 1 + lwp/app/__init__.py | 12 + lwp/app/views.py | 862 +++++++++++++++++++++++++++++++++++++ lwp.db => resources/lwp.db | Bin setup.py | 45 ++ 6 files changed, 923 insertions(+) create mode 100644 MANIFEST.in create mode 100644 lwp/__init__.py create mode 100644 lwp/app/__init__.py create mode 100644 lwp/app/views.py rename lwp.db => resources/lwp.db (100%) create mode 100644 setup.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5123cac --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +recursive-include lwp/templates * +recursive-include lwp/static * +recursive-include resources * \ No newline at end of file diff --git a/lwp/__init__.py b/lwp/__init__.py new file mode 100644 index 0000000..bca5f67 --- /dev/null +++ b/lwp/__init__.py @@ -0,0 +1 @@ +# encoding: utf-8 diff --git a/lwp/app/__init__.py b/lwp/app/__init__.py new file mode 100644 index 0000000..fa1ed66 --- /dev/null +++ b/lwp/app/__init__.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# encoding: utf-8 +import flask +import os + +# Flask app +TEMPLATE_PATH = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'templates')) +print TEMPLATE_PATH +app = flask.Flask('lwp', template_folder=TEMPLATE_PATH) +app.config.from_object('lwp') + +from views import * diff --git a/lwp/app/views.py b/lwp/app/views.py new file mode 100644 index 0000000..543f68b --- /dev/null +++ b/lwp/app/views.py @@ -0,0 +1,862 @@ +#!/usr/bin/env python +# encoding: utf-8 +from __future__ import absolute_import + +import lwp.lxclite as lxc +import lwp.lxc as lwp +import subprocess +import time +import re +import hashlib +import sqlite3 +import os + +from flask import request, session, g, redirect, url_for, abort, render_template, flash, jsonify + + +from . import app + +def connect_db(): + ''' + SQLite3 connect function + ''' + + return sqlite3.connect(app.options.db) + + +@app.before_request +def before_request(): + ''' + executes functions before all requests + ''' + + check_session_limit() + g.db = connect_db() + + +@app.teardown_request +def teardown_request(exception): + ''' + executes functions after all requests + ''' + + if hasattr(g, 'db'): + g.db.close() + + +@app.route('/') +@app.route('/home') +def home(): + ''' + home page function + ''' + + if 'logged_in' in session: + listx = lxc.listx() + containers_all = [] + + for status in ['RUNNING', 'FROZEN', 'STOPPED']: + containers_by_status = [] + + for container in listx[status]: + containers_by_status.append({ + 'name': container, + 'memusg': lwp.memory_usage(container), + 'settings': lwp.get_container_settings(container) + }) + containers_all.append({ + 'status': status.lower(), + 'containers': containers_by_status + }) + + return render_template('index.html', containers=lxc.ls(), + containers_all=containers_all, + dist=lwp.check_ubuntu(), + templates=lwp.get_templates_list()) + return render_template('login.html') + + +@app.route('/about') +def about(): + ''' + about page + ''' + + if 'logged_in' in session: + return render_template('about.html', containers=lxc.ls(), + version=lwp.check_version()) + return render_template('login.html') + + +@app.route('//edit', methods=['POST', 'GET']) +def edit(container=None): + ''' + edit containers page and actions if form post request + ''' + + if 'logged_in' in session: + host_memory = lwp.host_memory_usage() + if request.method == 'POST': + cfg = lwp.get_container_settings(container) + 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]?)(/(3[0-2]|[12]?[0-9]))?' + info = lxc.info(container) + + form = {} + form['type'] = request.form['type'] + form['link'] = request.form['link'] + try: + form['flags'] = request.form['flags'] + except KeyError: + form['flags'] = 'down' + form['hwaddr'] = request.form['hwaddress'] + form['rootfs'] = request.form['rootfs'] + form['utsname'] = request.form['hostname'] + form['ipv4'] = request.form['ipaddress'] + form['memlimit'] = request.form['memlimit'] + form['swlimit'] = request.form['swlimit'] + form['cpus'] = request.form['cpus'] + form['shares'] = request.form['cpushares'] + try: + form['autostart'] = request.form['autostart'] + except KeyError: + form['autostart'] = False + + if form['utsname'] != cfg['utsname'] and \ + re.match('(?!^containers$)|^(([a-zA-Z0-9]|[a-zA-Z0-9]' + '[a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|' + '[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$', + form['utsname']): + lwp.push_config_value('lxc.utsname', form['utsname'], + container=container) + flash(u'Hostname updated for %s!' % container, 'success') + + if form['flags'] != cfg['flags'] and \ + re.match('^(up|down)$', form['flags']): + lwp.push_config_value('lxc.network.flags', form['flags'], + container=container) + flash(u'Network flag updated for %s!' % container, 'success') + + if form['type'] != cfg['type'] and \ + re.match('^\w+$', form['type']): + lwp.push_config_value('lxc.network.type', form['type'], + container=container) + flash(u'Link type updated for %s!' % container, 'success') + + if form['link'] != cfg['link'] and \ + re.match('^[a-zA-Z0-9_-]+$', form['link']): + lwp.push_config_value('lxc.network.link', form['link'], + container=container) + flash(u'Link name updated for %s!' % container, 'success') + + if form['hwaddr'] != cfg['hwaddr'] and \ + re.match('^([a-fA-F0-9]{2}[:|\-]?){6}$', form['hwaddr']): + lwp.push_config_value('lxc.network.hwaddr', form['hwaddr'], + container=container) + flash(u'Hardware address updated for %s!' % container, + 'success') + + if (not form['ipv4'] and form['ipv4'] != cfg['ipv4']) or \ + (form['ipv4'] != cfg['ipv4'] and + re.match('^%s$' % ip_regex, form['ipv4'])): + lwp.push_config_value('lxc.network.ipv4', form['ipv4'], + container=container) + flash(u'IP address updated for %s!' % container, 'success') + + if form['memlimit'] != cfg['memlimit'] and \ + form['memlimit'].isdigit() and \ + int(form['memlimit']) <= int(host_memory['total']): + if int(form['memlimit']) == int(host_memory['total']): + form['memlimit'] = '' + + if form['memlimit'] != cfg['memlimit']: + lwp.push_config_value('lxc.cgroup.memory.limit_in_bytes', + form['memlimit'], + container=container) + if info["state"].lower() != 'stopped': + lxc.cgroup(container, + 'lxc.cgroup.memory.limit_in_bytes', + form['memlimit']) + flash(u'Memory limit updated for %s!' % container, + 'success') + + if form['swlimit'] != cfg['swlimit'] and \ + form['swlimit'].isdigit() and \ + int(form['swlimit']) <= int(host_memory['total'] * 2): + if int(form['swlimit']) == int(host_memory['total'] * 2): + form['swlimit'] = '' + + if form['swlimit'].isdigit(): + form['swlimit'] = int(form['swlimit']) + + if form['memlimit'].isdigit(): + form['memlimit'] = int(form['memlimit']) + + if (form['memlimit'] == '' and form['swlimit'] != '') or \ + (form['memlimit'] > form['swlimit'] and + form['swlimit'] != ''): + flash(u'Can\'t assign swap memory lower than' + ' the memory limit', 'warning') + + elif form['swlimit'] != cfg['swlimit'] and \ + form['memlimit'] <= form['swlimit']: + lwp.push_config_value( + 'lxc.cgroup.memory.memsw.limit_in_bytes', + form['swlimit'], container=container) + + if info["state"].lower() != 'stopped': + lxc.cgroup(container, + 'lxc.cgroup.memory.memsw.limit_in_bytes', + form['swlimit']) + flash(u'Swap limit updated for %s!' % container, 'success') + + if (not form['cpus'] and form['cpus'] != cfg['cpus']) or \ + (form['cpus'] != cfg['cpus'] and + re.match('^[0-9,-]+$', form['cpus'])): + lwp.push_config_value('lxc.cgroup.cpuset.cpus', form['cpus'], + container=container) + + if info["state"].lower() != 'stopped': + lxc.cgroup(container, 'lxc.cgroup.cpuset.cpus', + form['cpus']) + flash(u'CPUs updated for %s!' % container, 'success') + + if (not form['shares'] and form['shares'] != cfg['shares']) or \ + (form['shares'] != cfg['shares'] and + re.match('^[0-9]+$', form['shares'])): + lwp.push_config_value('lxc.cgroup.cpu.shares', form['shares'], + container=container) + if info["state"].lower() != 'stopped': + lxc.cgroup(container, 'lxc.cgroup.cpu.shares', + form['shares']) + flash(u'CPU shares updated for %s!' % container, 'success') + + if form['rootfs'] != cfg['rootfs'] and \ + re.match('^[a-zA-Z0-9_/\-\.]+', form['rootfs']): + lwp.push_config_value('lxc.rootfs', form['rootfs'], + container=container) + flash(u'Rootfs updated!' % container, 'success') + + auto = lwp.ls_auto() + if form['autostart'] == 'True' and \ + not ('%s.conf' % container) in auto: + try: + os.symlink('/var/lib/lxc/%s/config' % container, + '/etc/lxc/auto/%s.conf' % container) + flash(u'Autostart enabled for %s' % container, 'success') + except OSError: + flash(u'Unable to create symlink \'/etc/lxc/auto/%s.conf\'' + % container, 'error') + elif not form['autostart'] and ('%s.conf' % container) in auto: + try: + os.remove('/etc/lxc/auto/%s.conf' % container) + flash(u'Autostart disabled for %s' % container, 'success') + except OSError: + flash(u'Unable to remove symlink', 'error') + + info = lxc.info(container) + status = info['state'] + pid = info['pid'] + + infos = {'status': status, + 'pid': pid, + 'memusg': lwp.memory_usage(container)} + return render_template('edit.html', containers=lxc.ls(), + container=container, infos=infos, + settings=lwp.get_container_settings(container), + host_memory=host_memory) + return render_template('login.html') + + +@app.route('/settings/lxc-net', methods=['POST', 'GET']) +def lxc_net(): + ''' + lxc-net (/etc/default/lxc) settings page and actions if form post request + ''' + if 'logged_in' in session: + if session['su'] != 'Yes': + return abort(403) + + if request.method == 'POST': + if lxc.running() == []: + cfg = lwp.get_net_settings() + 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]?)' + + form = {} + try: + form['use'] = request.form['use'] + except KeyError: + form['use'] = 'false' + + try: + form['bridge'] = request.form['bridge'] + except KeyError: + form['bridge'] = None + + try: + form['address'] = request.form['address'] + except KeyError: + form['address'] = None + + try: + form['netmask'] = request.form['netmask'] + except KeyError: + form['netmask'] = None + + try: + form['network'] = request.form['network'] + except KeyError: + form['network'] = None + + try: + form['range'] = request.form['range'] + except KeyError: + form['range'] = None + + try: + form['max'] = request.form['max'] + except KeyError: + form['max'] = None + + if form['use'] == 'true' and form['use'] != cfg['use']: + lwp.push_net_value('USE_LXC_BRIDGE', 'true') + + elif form['use'] == 'false' and form['use'] != cfg['use']: + lwp.push_net_value('USE_LXC_BRIDGE', 'false') + + if form['bridge'] and form['bridge'] != cfg['bridge'] \ + and re.match('^[a-zA-Z0-9_-]+$', form['bridge']): + lwp.push_net_value('LXC_BRIDGE', form['bridge']) + + if form['address'] and form['address'] != cfg['address'] \ + and re.match('^%s$' % ip_regex, form['address']): + lwp.push_net_value('LXC_ADDR', form['address']) + + if form['netmask'] and form['netmask'] != cfg['netmask'] \ + and re.match('^%s$' % ip_regex, form['netmask']): + lwp.push_net_value('LXC_NETMASK', form['netmask']) + + if form['network'] and form['network'] != cfg['network'] and \ + re.match('^%s(?:/\d{1,2}|)$' % ip_regex, + form['network']): + lwp.push_net_value('LXC_NETWORK', form['network']) + + if form['range'] and form['range'] != cfg['range'] and \ + re.match('^%s,%s$' % (ip_regex, ip_regex), + form['range']): + lwp.push_net_value('LXC_DHCP_RANGE', form['range']) + + if form['max'] and form['max'] != cfg['max'] and \ + re.match('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + form['max']): + lwp.push_net_value('LXC_DHCP_MAX', form['max']) + + if lwp.net_restart() == 0: + flash(u'LXC Network settings applied successfully!', + 'success') + else: + flash(u'Failed to restart LXC networking.', 'error') + else: + flash(u'Stop all containers before restart lxc-net.', + 'warning') + return render_template('lxc-net.html', containers=lxc.ls(), + cfg=lwp.get_net_settings(), + running=lxc.running()) + return render_template('login.html') + + +@app.route('/lwp/users', methods=['POST', 'GET']) +def lwp_users(): + ''' + returns users and get posts request : can edit or add user in page. + this funtction uses sqlite3 + ''' + if 'logged_in' in session: + if session['su'] != 'Yes': + return abort(403) + + try: + trash = request.args.get('trash') + except KeyError: + trash = 0 + + su_users = query_db("SELECT COUNT(id) as num FROM users " + "WHERE su='Yes'", [], one=True) + + if request.args.get('token') == session.get('token') and \ + int(trash) == 1 and request.args.get('userid') and \ + request.args.get('username'): + nb_users = query_db("SELECT COUNT(id) as num FROM users", [], + one=True) + + if nb_users['num'] > 1: + if su_users['num'] <= 1: + su_user = query_db("SELECT username FROM users " + "WHERE su='Yes'", [], one=True) + + if su_user['username'] == request.args.get('username'): + flash(u'Can\'t delete the last admin user : %s' % + request.args.get('username'), 'error') + return redirect(url_for('lwp_users')) + + g.db.execute("DELETE FROM users WHERE id=? AND username=?", + [request.args.get('userid'), + request.args.get('username')]) + g.db.commit() + flash(u'Deleted %s' % request.args.get('username'), 'success') + return redirect(url_for('lwp_users')) + + flash(u'Can\'t delete the last user!', 'error') + return redirect(url_for('lwp_users')) + + if request.method == 'POST': + users = query_db('SELECT id, name, username, su FROM users ' + 'ORDER BY id ASC') + + if request.form['newUser'] == 'True': + if not request.form['username'] in \ + [user['username'] for user in users]: + if re.match('^\w+$', request.form['username']) and \ + request.form['password1']: + if request.form['password1'] == \ + request.form['password2']: + if request.form['name']: + if re.match('[a-z A-Z0-9]{3,32}', + request.form['name']): + g.db.execute( + "INSERT INTO users " + "(name, username, password) " + "VALUES (?, ?, ?)", + [request.form['name'], + request.form['username'], + hash_passwd( + request.form['password1'])]) + g.db.commit() + else: + flash(u'Invalid name!', 'error') + else: + g.db.execute("INSERT INTO users " + "(username, password) VALUES " + "(?, ?)", + [request.form['username'], + hash_passwd( + request.form['password1'])]) + g.db.commit() + + flash(u'Created %s' % request.form['username'], + 'success') + else: + flash(u'No password match', 'error') + else: + flash(u'Invalid username or password!', 'error') + else: + flash(u'Username already exist!', 'error') + + elif request.form['newUser'] == 'False': + if request.form['password1'] == request.form['password2']: + if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): + if su_users['num'] <= 1: + su = 'Yes' + else: + try: + su = request.form['su'] + except KeyError: + su = 'No' + + if not request.form['name']: + g.db.execute("UPDATE users SET name='', su=? " + "WHERE username=?", + [su, request.form['username']]) + g.db.commit() + elif request.form['name'] and \ + not request.form['password1'] and \ + not request.form['password2']: + g.db.execute("UPDATE users SET name=?, su=? " + "WHERE username=?", + [request.form['name'], su, + request.form['username']]) + g.db.commit() + elif request.form['name'] and \ + request.form['password1'] and \ + request.form['password2']: + g.db.execute("UPDATE users SET " + "name=?, password=?, su=? WHERE " + "username=?", + [request.form['name'], + hash_passwd( + request.form['password1']), + su, request.form['username']]) + g.db.commit() + elif request.form['password1'] and \ + request.form['password2']: + g.db.execute("UPDATE users SET password=?, su=? " + "WHERE username=?", + [hash_passwd( + request.form['password1']), + su, request.form['username']]) + g.db.commit() + + flash(u'Updated', 'success') + else: + flash(u'Invalid name!', 'error') + else: + flash(u'No password match', 'error') + else: + flash(u'Unknown error!', 'error') + + users = query_db("SELECT id, name, username, su FROM users " + "ORDER BY id ASC") + nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) + su_users = query_db("SELECT COUNT(id) as num FROM users " + "WHERE su='Yes'", [], one=True) + + return render_template('users.html', containers=lxc.ls(), users=users, + nb_users=nb_users, su_users=su_users) + return render_template('login.html') + + +@app.route('/checkconfig') +def checkconfig(): + ''' + returns the display of lxc-checkconfig command + ''' + if 'logged_in' in session: + if session['su'] != 'Yes': + return abort(403) + + return render_template('checkconfig.html', containers=lxc.ls(), + cfg=lxc.checkconfig()) + return render_template('login.html') + + +@app.route('/action', methods=['GET']) +def action(): + ''' + manage all actions related to containers + lxc-start, lxc-stop, etc... + ''' + if 'logged_in' in session: + if request.args['token'] == session.get('token'): + action = request.args['action'] + name = request.args['name'] + + if action == 'start': + try: + if lxc.start(name) == 0: + # Fix bug : "the container is randomly not + # displayed in overview list after a boot" + time.sleep(1) + flash(u'Container %s started successfully!' % name, + 'success') + else: + flash(u'Unable to start %s!' % name, 'error') + except lxc.ContainerAlreadyRunning: + flash(u'Container %s is already running!' % name, 'error') + elif action == 'stop': + try: + if lxc.stop(name) == 0: + flash(u'Container %s stopped successfully!' % name, + 'success') + else: + flash(u'Unable to stop %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s is already stopped!' % name, 'error') + elif action == 'freeze': + try: + if lxc.freeze(name) == 0: + flash(u'Container %s frozen successfully!' % name, + 'success') + else: + flash(u'Unable to freeze %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s not running!' % name, 'error') + elif action == 'unfreeze': + try: + if lxc.unfreeze(name) == 0: + flash(u'Container %s unfrozen successfully!' % name, + 'success') + else: + flash(u'Unable to unfeeze %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s not frozen!' % name, 'error') + elif action == 'destroy': + if session['su'] != 'Yes': + return abort(403) + try: + if lxc.destroy(name) == 0: + flash(u'Container %s destroyed successfully!' % name, + 'success') + else: + flash(u'Unable to destroy %s!' % name, 'error') + except lxc.ContainerDoesntExists: + flash(u'The Container %s does not exists!' % name, 'error') + elif action == 'reboot' and name == 'host': + if session['su'] != 'Yes': + return abort(403) + msg = '\v*** LXC Web Panel *** \ + \nReboot from web panel' + try: + subprocess.check_call('/sbin/shutdown -r now \'%s\'' % msg, + shell=True) + flash(u'System will now restart!', 'success') + except: + flash(u'System error!', 'error') + try: + if request.args['from'] == 'edit': + return redirect('../%s/edit' % name) + else: + return redirect(url_for('home')) + except: + return redirect(url_for('home')) + return render_template('login.html') + + +@app.route('/action/create-container', methods=['GET', 'POST']) +def create_container(): + ''' + verify all forms to create a container + ''' + if 'logged_in' in session: + if session['su'] != 'Yes': + return abort(403) + if request.method == 'POST': + name = request.form['name'] + template = request.form['template'] + command = request.form['command'] + + if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): + storage_method = request.form['backingstore'] + + if storage_method == 'default': + try: + if lxc.create(name, template=template, + xargs=command) == 0: + flash(u'Container %s created successfully!' % name, + 'success') + else: + flash(u'Failed to create %s!' % name, 'error') + except lxc.ContainerAlreadyExists: + flash(u'The Container %s is already created!' % name, + 'error') + except subprocess.CalledProcessError: + flash(u'Error!' % name, 'error') + + elif storage_method == 'directory': + directory = request.form['dir'] + + if re.match('^/[a-zA-Z0-9_/-]+$', directory) and \ + directory != '': + try: + if lxc.create(name, template=template, + storage='dir --dir %s' % directory, + xargs=command) == 0: + flash(u'Container %s created successfully!' + % name, 'success') + else: + flash(u'Failed to create %s!' % name, 'error') + except lxc.ContainerAlreadyExists: + flash(u'The Container %s is already created!' + % name, 'error') + except subprocess.CalledProcessError: + flash(u'Error!' % name, 'error') + + elif storage_method == 'lvm': + lvname = request.form['lvname'] + vgname = request.form['vgname'] + fstype = request.form['fstype'] + fssize = request.form['fssize'] + storage_options = 'lvm' + + if re.match('^[a-zA-Z0-9_-]+$', lvname) and lvname != '': + storage_options += ' --lvname %s' % lvname + if re.match('^[a-zA-Z0-9_-]+$', vgname) and vgname != '': + storage_options += ' --vgname %s' % vgname + if re.match('^[a-z0-9]+$', fstype) and fstype != '': + storage_options += ' --fstype %s' % fstype + if re.match('^[0-9][G|M]$', fssize) and fssize != '': + storage_options += ' --fssize %s' % fssize + + try: + if lxc.create(name, template=template, + storage=storage_options, + xargs=command) == 0: + flash(u'Container %s created successfully!' % name, + 'success') + else: + flash(u'Failed to create %s!' % name, 'error') + except lxc.ContainerAlreadyExists: + flash(u'The container/logical volume %s is ' + 'already created!' % name, 'error') + except subprocess.CalledProcessError: + flash(u'Error!' % name, 'error') + + else: + flash(u'Missing parameters to create container!', 'error') + + else: + if name == '': + flash(u'Please enter a container name!', 'error') + else: + flash(u'Invalid name for \"%s\"!' % name, 'error') + + return redirect(url_for('home')) + return render_template('login.html') + + +@app.route('/action/clone-container', methods=['GET', 'POST']) +def clone_container(): + ''' + verify all forms to clone a container + ''' + if 'logged_in' in session: + if session['su'] != 'Yes': + return abort(403) + if request.method == 'POST': + orig = request.form['orig'] + name = request.form['name'] + + try: + snapshot = request.form['snapshot'] + if snapshot == 'True': + snapshot = True + except KeyError: + snapshot = False + + if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): + out = None + + try: + out = lxc.clone(orig=orig, new=name, snapshot=snapshot) + except lxc.ContainerAlreadyExists: + flash(u'The Container %s already exists!' % name, 'error') + except subprocess.CalledProcessError: + flash(u'Can\'t snapshot a directory', 'error') + + if out and out == 0: + flash(u'Container %s cloned into %s successfully!' + % (orig, name), 'success') + elif out and out != 0: + flash(u'Failed to clone %s into %s!' % (orig, name), + 'error') + + else: + if name == '': + flash(u'Please enter a container name!', 'error') + else: + flash(u'Invalid name for \"%s\"!' % name, 'error') + + return redirect(url_for('home')) + return render_template('login.html') + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + request_username = request.form['username'] + request_passwd = hash_passwd(request.form['password']) + + current_url = request.form['url'] + + user = query_db('select name, username, su from users where username=?' + 'and password=?', [request_username, request_passwd], + one=True) + + if user: + session['logged_in'] = True + session['token'] = get_token() + session['last_activity'] = int(time.time()) + session['username'] = user['username'] + session['name'] = user['name'] + session['su'] = user['su'] + flash(u'You are logged in!', 'success') + + if current_url == url_for('login'): + return redirect(url_for('home')) + return redirect(current_url) + + flash(u'Invalid username or password!', 'error') + return render_template('login.html') + + +@app.route('/logout') +def logout(): + session.pop('logged_in', None) + session.pop('token', None) + session.pop('last_activity', None) + session.pop('username', None) + session.pop('name', None) + session.pop('su', None) + flash(u'You are logged out!', 'success') + return redirect(url_for('login')) + + +@app.route('/_refresh_cpu_host') +def refresh_cpu_host(): + if 'logged_in' in session: + return lwp.host_cpu_percent() + + +@app.route('/_refresh_uptime_host') +def refresh_uptime_host(): + if 'logged_in' in session: + return jsonify(lwp.host_uptime()) + + +@app.route('/_refresh_disk_host') +def refresh_disk_host(): + if 'logged_in' in session: + return jsonify(lwp.host_disk_usage(directory=app.options.directory)) + + +@app.route('/_refresh_memory_') +def refresh_memory_containers(name=None): + if 'logged_in' in session: + if name == 'containers': + containers_running = lxc.running() + containers = [] + for container in containers_running: + container = container.replace(' (auto)', '') + containers.append({'name': container, + 'memusg': lwp.memory_usage(container)}) + return jsonify(data=containers) + elif name == 'host': + return jsonify(lwp.host_memory_usage()) + return jsonify({'memusg': lwp.memory_usage(name)}) + + +@app.route('/_check_version') +def check_version(): + if 'logged_in' in session: + return jsonify(lwp.check_version()) + + +def hash_passwd(passwd): + return hashlib.sha512(passwd.encode()).hexdigest() + + +def get_token(): + return hashlib.md5(str(time.time()).encode()).hexdigest() + + +def query_db(query, args=(), one=False): + cur = g.db.execute(query, args) + rv = [dict((cur.description[idx][0], value) + for idx, value in enumerate(row)) for row in cur.fetchall()] + return (rv[0] if rv else None) if one else rv + + +def check_session_limit(): + if 'logged_in' in session and session.get('last_activity') is not None: + now = int(time.time()) + limit = now - 60 * int(app.options.session_timeout) + last_activity = session.get('last_activity') + if last_activity < limit: + flash(u'Session timed out !', 'info') + logout() + else: + session['last_activity'] = now diff --git a/lwp.db b/resources/lwp.db similarity index 100% rename from lwp.db rename to resources/lwp.db diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..933a77a --- /dev/null +++ b/setup.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# encoding: utf-8 +from __future__ import absolute_import, print_function +import os + +try: + from setuptools import setup, find_packages +except ImportError: + from distutils.core import setup + + +__version__ = '0.2-pre0' +__author__ = 'Élie Deloumeau, Antoine Tanzilli' + + +supports = { + 'install_requires': [ + 'flask==0.9', + 'arconfig', + ] +} + +data_files = [] +if not os.path.exists('/var/lib/lxc/lwp.db'): + data_files.append(('/var/lib/lxc/', ['resources/lwp.db'])) + +setup( + name='lwp', + version=__version__, + author=__author__, + license="MIT", + description="LXC Web Interface", + platforms="linux", + classifiers=[ + 'Environment :: Console', + 'Programming Language :: Python', + ], + scripts=['bin/lwp'], + include_package_data=True, + zip_safe=False, + data_files=data_files, + packages=find_packages(), + **supports +) + From d218a33dc0ad04f966a3cc06061b054d72524348 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 08:57:09 +0300 Subject: [PATCH 09/20] [fix] small refactor --- lwp/app/views.py | 822 ++++++++++++++++++++++---------------------- lwp/lxc/__init__.py | 26 +- 2 files changed, 419 insertions(+), 429 deletions(-) diff --git a/lwp/app/views.py b/lwp/app/views.py index 543f68b..79a600e 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -2,6 +2,7 @@ # encoding: utf-8 from __future__ import absolute_import +from functools import wraps import lwp.lxclite as lxc import lwp.lxc as lwp import subprocess @@ -12,9 +13,29 @@ import os from flask import request, session, g, redirect, url_for, abort, render_template, flash, jsonify +from . import app -from . import app +def if_superuser(func): + @wraps(func) + def wrap(*args, **kwargs): + if session['su'] != 'Yes': + return abort(403) + else: + return func(*args, **kwargs) + return wrap + + +def if_auth(func): + @wraps(func) + def wrap(*args, **kwargs): + if 'logged_in' in session: + return func(*args, **kwargs) + else: + return render_template('login.html') + + return wrap + def connect_db(): ''' @@ -44,480 +65,449 @@ def teardown_request(exception): g.db.close() -@app.route('/') @app.route('/home') def home(): ''' home page function ''' - if 'logged_in' in session: - listx = lxc.listx() - containers_all = [] - - for status in ['RUNNING', 'FROZEN', 'STOPPED']: - containers_by_status = [] - - for container in listx[status]: - containers_by_status.append({ - 'name': container, - 'memusg': lwp.memory_usage(container), - 'settings': lwp.get_container_settings(container) - }) - containers_all.append({ - 'status': status.lower(), - 'containers': containers_by_status + listx = lxc.listx() + containers_all = [] + + for status in ['RUNNING', 'FROZEN', 'STOPPED']: + containers_by_status = [] + + for container in listx[status]: + containers_by_status.append({ + 'name': container, + 'memusg': lwp.memory_usage(container), + 'settings': lwp.get_container_settings(container) }) + containers_all.append({ + 'status': status.lower(), + 'containers': containers_by_status + }) - return render_template('index.html', containers=lxc.ls(), - containers_all=containers_all, - dist=lwp.check_ubuntu(), - templates=lwp.get_templates_list()) - return render_template('login.html') + return render_template('index.html', containers=lxc.ls(), + containers_all=containers_all, + dist=lwp.check_ubuntu(), + templates=lwp.get_templates_list()) + + +@if_auth +@app.route('/') +def index(): + return redirect(url_for(home), code=302) +@if_auth @app.route('/about') def about(): ''' about page ''' - if 'logged_in' in session: - return render_template('about.html', containers=lxc.ls(), - version=lwp.check_version()) - return render_template('login.html') + return render_template('about.html', containers=lxc.ls(), version=lwp.check_version()) +@if_auth @app.route('//edit', methods=['POST', 'GET']) def edit(container=None): ''' edit containers page and actions if form post request ''' - if 'logged_in' in session: - host_memory = lwp.host_memory_usage() - if request.method == 'POST': - cfg = lwp.get_container_settings(container) - 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]?)(/(3[0-2]|[12]?[0-9]))?' - info = lxc.info(container) - - form = {} - form['type'] = request.form['type'] - form['link'] = request.form['link'] - try: - form['flags'] = request.form['flags'] - except KeyError: - form['flags'] = 'down' - form['hwaddr'] = request.form['hwaddress'] - form['rootfs'] = request.form['rootfs'] - form['utsname'] = request.form['hostname'] - form['ipv4'] = request.form['ipaddress'] - form['memlimit'] = request.form['memlimit'] - form['swlimit'] = request.form['swlimit'] - form['cpus'] = request.form['cpus'] - form['shares'] = request.form['cpushares'] - try: - form['autostart'] = request.form['autostart'] - except KeyError: - form['autostart'] = False - - if form['utsname'] != cfg['utsname'] and \ - re.match('(?!^containers$)|^(([a-zA-Z0-9]|[a-zA-Z0-9]' - '[a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|' - '[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$', - form['utsname']): - lwp.push_config_value('lxc.utsname', form['utsname'], - container=container) - flash(u'Hostname updated for %s!' % container, 'success') - - if form['flags'] != cfg['flags'] and \ - re.match('^(up|down)$', form['flags']): - lwp.push_config_value('lxc.network.flags', form['flags'], - container=container) - flash(u'Network flag updated for %s!' % container, 'success') - - if form['type'] != cfg['type'] and \ - re.match('^\w+$', form['type']): - lwp.push_config_value('lxc.network.type', form['type'], - container=container) - flash(u'Link type updated for %s!' % container, 'success') - - if form['link'] != cfg['link'] and \ - re.match('^[a-zA-Z0-9_-]+$', form['link']): - lwp.push_config_value('lxc.network.link', form['link'], - container=container) - flash(u'Link name updated for %s!' % container, 'success') + host_memory = lwp.host_memory_usage() + if request.method == 'POST': + cfg = lwp.get_container_settings(container) + 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]?)(/(3[0-2]|[12]?[0-9]))?' + info = lxc.info(container) - if form['hwaddr'] != cfg['hwaddr'] and \ - re.match('^([a-fA-F0-9]{2}[:|\-]?){6}$', form['hwaddr']): - lwp.push_config_value('lxc.network.hwaddr', form['hwaddr'], + form = {} + form['type'] = request.form['type'] + form['link'] = request.form['link'] + try: + form['flags'] = request.form['flags'] + except KeyError: + form['flags'] = 'down' + form['hwaddr'] = request.form['hwaddress'] + form['rootfs'] = request.form['rootfs'] + form['utsname'] = request.form['hostname'] + form['ipv4'] = request.form['ipaddress'] + form['memlimit'] = request.form['memlimit'] + form['swlimit'] = request.form['swlimit'] + form['cpus'] = request.form['cpus'] + form['shares'] = request.form['cpushares'] + try: + form['autostart'] = request.form['autostart'] + except KeyError: + form['autostart'] = False + + if form['utsname'] != cfg['utsname'] and \ + re.match('(?!^containers$)|^(([a-zA-Z0-9]|[a-zA-Z0-9]' + '[a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|' + '[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$', + form['utsname']): + lwp.push_config_value('lxc.utsname', form['utsname'], + container=container) + flash(u'Hostname updated for %s!' % container, 'success') + + if form['flags'] != cfg['flags'] and \ + re.match('^(up|down)$', form['flags']): + lwp.push_config_value('lxc.network.flags', form['flags'], + container=container) + flash(u'Network flag updated for %s!' % container, 'success') + + if form['type'] != cfg['type'] and \ + re.match('^\w+$', form['type']): + lwp.push_config_value('lxc.network.type', form['type'], + container=container) + flash(u'Link type updated for %s!' % container, 'success') + + if form['link'] != cfg['link'] and \ + re.match('^[a-zA-Z0-9_-]+$', form['link']): + lwp.push_config_value('lxc.network.link', form['link'], + container=container) + flash(u'Link name updated for %s!' % container, 'success') + + if form['hwaddr'] != cfg['hwaddr'] and \ + re.match('^([a-fA-F0-9]{2}[:|\-]?){6}$', form['hwaddr']): + lwp.push_config_value('lxc.network.hwaddr', form['hwaddr'], + container=container) + flash(u'Hardware address updated for %s!' % container, + 'success') + + if (not form['ipv4'] and form['ipv4'] != cfg['ipv4']) or \ + (form['ipv4'] != cfg['ipv4'] and + re.match('^%s$' % ip_regex, form['ipv4'])): + lwp.push_config_value('lxc.network.ipv4', form['ipv4'], + container=container) + flash(u'IP address updated for %s!' % container, 'success') + + if form['memlimit'] != cfg['memlimit'] and \ + form['memlimit'].isdigit() and \ + int(form['memlimit']) <= int(host_memory['total']): + if int(form['memlimit']) == int(host_memory['total']): + form['memlimit'] = '' + + if form['memlimit'] != cfg['memlimit']: + lwp.push_config_value('lxc.cgroup.memory.limit_in_bytes', + form['memlimit'], container=container) - flash(u'Hardware address updated for %s!' % container, + if info["state"].lower() != 'stopped': + lxc.cgroup(container, + 'lxc.cgroup.memory.limit_in_bytes', + form['memlimit']) + flash(u'Memory limit updated for %s!' % container, 'success') - if (not form['ipv4'] and form['ipv4'] != cfg['ipv4']) or \ - (form['ipv4'] != cfg['ipv4'] and - re.match('^%s$' % ip_regex, form['ipv4'])): - lwp.push_config_value('lxc.network.ipv4', form['ipv4'], - container=container) - flash(u'IP address updated for %s!' % container, 'success') - - if form['memlimit'] != cfg['memlimit'] and \ - form['memlimit'].isdigit() and \ - int(form['memlimit']) <= int(host_memory['total']): - if int(form['memlimit']) == int(host_memory['total']): - form['memlimit'] = '' - - if form['memlimit'] != cfg['memlimit']: - lwp.push_config_value('lxc.cgroup.memory.limit_in_bytes', - form['memlimit'], - container=container) - if info["state"].lower() != 'stopped': - lxc.cgroup(container, - 'lxc.cgroup.memory.limit_in_bytes', - form['memlimit']) - flash(u'Memory limit updated for %s!' % container, - 'success') - - if form['swlimit'] != cfg['swlimit'] and \ - form['swlimit'].isdigit() and \ - int(form['swlimit']) <= int(host_memory['total'] * 2): - if int(form['swlimit']) == int(host_memory['total'] * 2): - form['swlimit'] = '' - - if form['swlimit'].isdigit(): - form['swlimit'] = int(form['swlimit']) - - if form['memlimit'].isdigit(): - form['memlimit'] = int(form['memlimit']) - - if (form['memlimit'] == '' and form['swlimit'] != '') or \ - (form['memlimit'] > form['swlimit'] and - form['swlimit'] != ''): - flash(u'Can\'t assign swap memory lower than' - ' the memory limit', 'warning') - - elif form['swlimit'] != cfg['swlimit'] and \ - form['memlimit'] <= form['swlimit']: - lwp.push_config_value( - 'lxc.cgroup.memory.memsw.limit_in_bytes', - form['swlimit'], container=container) - - if info["state"].lower() != 'stopped': - lxc.cgroup(container, - 'lxc.cgroup.memory.memsw.limit_in_bytes', - form['swlimit']) - flash(u'Swap limit updated for %s!' % container, 'success') - - if (not form['cpus'] and form['cpus'] != cfg['cpus']) or \ - (form['cpus'] != cfg['cpus'] and - re.match('^[0-9,-]+$', form['cpus'])): - lwp.push_config_value('lxc.cgroup.cpuset.cpus', form['cpus'], - container=container) + if form['swlimit'] != cfg['swlimit'] and \ + form['swlimit'].isdigit() and \ + int(form['swlimit']) <= int(host_memory['total'] * 2): + if int(form['swlimit']) == int(host_memory['total'] * 2): + form['swlimit'] = '' - if info["state"].lower() != 'stopped': - lxc.cgroup(container, 'lxc.cgroup.cpuset.cpus', - form['cpus']) - flash(u'CPUs updated for %s!' % container, 'success') - - if (not form['shares'] and form['shares'] != cfg['shares']) or \ - (form['shares'] != cfg['shares'] and - re.match('^[0-9]+$', form['shares'])): - lwp.push_config_value('lxc.cgroup.cpu.shares', form['shares'], - container=container) - if info["state"].lower() != 'stopped': - lxc.cgroup(container, 'lxc.cgroup.cpu.shares', - form['shares']) - flash(u'CPU shares updated for %s!' % container, 'success') + if form['swlimit'].isdigit(): + form['swlimit'] = int(form['swlimit']) - if form['rootfs'] != cfg['rootfs'] and \ - re.match('^[a-zA-Z0-9_/\-\.]+', form['rootfs']): - lwp.push_config_value('lxc.rootfs', form['rootfs'], - container=container) - flash(u'Rootfs updated!' % container, 'success') + if form['memlimit'].isdigit(): + form['memlimit'] = int(form['memlimit']) - auto = lwp.ls_auto() - if form['autostart'] == 'True' and \ - not ('%s.conf' % container) in auto: - try: - os.symlink('/var/lib/lxc/%s/config' % container, - '/etc/lxc/auto/%s.conf' % container) - flash(u'Autostart enabled for %s' % container, 'success') - except OSError: - flash(u'Unable to create symlink \'/etc/lxc/auto/%s.conf\'' - % container, 'error') - elif not form['autostart'] and ('%s.conf' % container) in auto: - try: - os.remove('/etc/lxc/auto/%s.conf' % container) - flash(u'Autostart disabled for %s' % container, 'success') - except OSError: - flash(u'Unable to remove symlink', 'error') - - info = lxc.info(container) - status = info['state'] - pid = info['pid'] - - infos = {'status': status, - 'pid': pid, - 'memusg': lwp.memory_usage(container)} - return render_template('edit.html', containers=lxc.ls(), - container=container, infos=infos, - settings=lwp.get_container_settings(container), - host_memory=host_memory) - return render_template('login.html') + if (form['memlimit'] == '' and form['swlimit'] != '') or \ + (form['memlimit'] > form['swlimit'] and + form['swlimit'] != ''): + flash(u'Can\'t assign swap memory lower than' + ' the memory limit', 'warning') + elif form['swlimit'] != cfg['swlimit'] and \ + form['memlimit'] <= form['swlimit']: + lwp.push_config_value( + 'lxc.cgroup.memory.memsw.limit_in_bytes', + form['swlimit'], container=container) + if info["state"].lower() != 'stopped': + lxc.cgroup(container, + 'lxc.cgroup.memory.memsw.limit_in_bytes', + form['swlimit']) + flash(u'Swap limit updated for %s!' % container, 'success') + + if (not form['cpus'] and form['cpus'] != cfg['cpus']) or \ + (form['cpus'] != cfg['cpus'] and + re.match('^[0-9,-]+$', form['cpus'])): + lwp.push_config_value('lxc.cgroup.cpuset.cpus', form['cpus'], + container=container) + + if info["state"].lower() != 'stopped': + lxc.cgroup(container, 'lxc.cgroup.cpuset.cpus', + form['cpus']) + flash(u'CPUs updated for %s!' % container, 'success') + + if (not form['shares'] and form['shares'] != cfg['shares']) or \ + (form['shares'] != cfg['shares'] and + re.match('^[0-9]+$', form['shares'])): + lwp.push_config_value('lxc.cgroup.cpu.shares', form['shares'], + container=container) + if info["state"].lower() != 'stopped': + lxc.cgroup(container, 'lxc.cgroup.cpu.shares', + form['shares']) + flash(u'CPU shares updated for %s!' % container, 'success') + + if form['rootfs'] != cfg['rootfs'] and \ + re.match('^[a-zA-Z0-9_/\-\.]+', form['rootfs']): + lwp.push_config_value('lxc.rootfs', form['rootfs'], + container=container) + flash(u'Rootfs updated!' % container, 'success') + + auto = lwp.ls_auto() + if form['autostart'] == 'True' and \ + not ('%s.conf' % container) in auto: + try: + os.symlink('/var/lib/lxc/%s/config' % container, + '/etc/lxc/auto/%s.conf' % container) + flash(u'Autostart enabled for %s' % container, 'success') + except OSError: + flash(u'Unable to create symlink \'/etc/lxc/auto/%s.conf\'' + % container, 'error') + elif not form['autostart'] and ('%s.conf' % container) in auto: + try: + os.remove('/etc/lxc/auto/%s.conf' % container) + flash(u'Autostart disabled for %s' % container, 'success') + except OSError: + flash(u'Unable to remove symlink', 'error') + + info = lxc.info(container) + status = info['state'] + pid = info['pid'] + + infos = {'status': status, + 'pid': pid, + 'memusg': lwp.memory_usage(container)} + return render_template('edit.html', containers=lxc.ls(), + container=container, infos=infos, + settings=lwp.get_container_settings(container), + host_memory=host_memory) + + +@if_superuser +@if_auth @app.route('/settings/lxc-net', methods=['POST', 'GET']) def lxc_net(): ''' lxc-net (/etc/default/lxc) settings page and actions if form post request ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - if request.method == 'POST': - if lxc.running() == []: - cfg = lwp.get_net_settings() - 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]?)' - - form = {} - try: - form['use'] = request.form['use'] - except KeyError: - form['use'] = 'false' - - try: - form['bridge'] = request.form['bridge'] - except KeyError: - form['bridge'] = None - - try: - form['address'] = request.form['address'] - except KeyError: - form['address'] = None - - try: - form['netmask'] = request.form['netmask'] - except KeyError: - form['netmask'] = None - try: - form['network'] = request.form['network'] - except KeyError: - form['network'] = None - - try: - form['range'] = request.form['range'] - except KeyError: - form['range'] = None - - try: - form['max'] = request.form['max'] - except KeyError: - form['max'] = None - - if form['use'] == 'true' and form['use'] != cfg['use']: - lwp.push_net_value('USE_LXC_BRIDGE', 'true') - - elif form['use'] == 'false' and form['use'] != cfg['use']: - lwp.push_net_value('USE_LXC_BRIDGE', 'false') - - if form['bridge'] and form['bridge'] != cfg['bridge'] \ - and re.match('^[a-zA-Z0-9_-]+$', form['bridge']): - lwp.push_net_value('LXC_BRIDGE', form['bridge']) - - if form['address'] and form['address'] != cfg['address'] \ - and re.match('^%s$' % ip_regex, form['address']): - lwp.push_net_value('LXC_ADDR', form['address']) - - if form['netmask'] and form['netmask'] != cfg['netmask'] \ - and re.match('^%s$' % ip_regex, form['netmask']): - lwp.push_net_value('LXC_NETMASK', form['netmask']) - - if form['network'] and form['network'] != cfg['network'] and \ - re.match('^%s(?:/\d{1,2}|)$' % ip_regex, - form['network']): - lwp.push_net_value('LXC_NETWORK', form['network']) - - if form['range'] and form['range'] != cfg['range'] and \ - re.match('^%s,%s$' % (ip_regex, ip_regex), - form['range']): - lwp.push_net_value('LXC_DHCP_RANGE', form['range']) - - if form['max'] and form['max'] != cfg['max'] and \ - re.match('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', - form['max']): - lwp.push_net_value('LXC_DHCP_MAX', form['max']) - - if lwp.net_restart() == 0: - flash(u'LXC Network settings applied successfully!', - 'success') - else: - flash(u'Failed to restart LXC networking.', 'error') + if request.method == 'POST': + if lxc.running() == []: + cfg = lwp.get_net_settings() + 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]?)' + + form = { + 'use': request.form.get('use', 'false') + 'bridge': request.form.get('bridge') + 'address': request.form.get('address'), + 'netmask': request.form.get('netmask'), + 'network': request.form.get('network'), + 'range': request.form.get('range'), + 'max': request.form.get('max'), + } + + if form['use'] == 'true' and form['use'] != cfg['use']: + lwp.push_net_value('USE_LXC_BRIDGE', 'true') + + elif form['use'] == 'false' and form['use'] != cfg['use']: + lwp.push_net_value('USE_LXC_BRIDGE', 'false') + + if form['bridge'] and form['bridge'] != cfg['bridge'] \ + and re.match('^[a-zA-Z0-9_-]+$', form['bridge']): + lwp.push_net_value('LXC_BRIDGE', form['bridge']) + + if form['address'] and form['address'] != cfg['address'] \ + and re.match('^%s$' % ip_regex, form['address']): + lwp.push_net_value('LXC_ADDR', form['address']) + + if form['netmask'] and form['netmask'] != cfg['netmask'] \ + and re.match('^%s$' % ip_regex, form['netmask']): + lwp.push_net_value('LXC_NETMASK', form['netmask']) + + if form['network'] and form['network'] != cfg['network'] and \ + re.match('^%s(?:/\d{1,2}|)$' % ip_regex, + form['network']): + lwp.push_net_value('LXC_NETWORK', form['network']) + + if form['range'] and form['range'] != cfg['range'] and \ + re.match('^%s,%s$' % (ip_regex, ip_regex), + form['range']): + lwp.push_net_value('LXC_DHCP_RANGE', form['range']) + + if form['max'] and form['max'] != cfg['max'] and \ + re.match('^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$', + form['max']): + lwp.push_net_value('LXC_DHCP_MAX', form['max']) + + if lwp.net_restart() == 0: + flash(u'LXC Network settings applied successfully!', + 'success') else: - flash(u'Stop all containers before restart lxc-net.', - 'warning') - return render_template('lxc-net.html', containers=lxc.ls(), - cfg=lwp.get_net_settings(), - running=lxc.running()) - return render_template('login.html') + flash(u'Failed to restart LXC networking.', 'error') + else: + flash(u'Stop all containers before restart lxc-net.', + 'warning') + return render_template('lxc-net.html', containers=lxc.ls(), + cfg=lwp.get_net_settings(), + running=lxc.running()) +@if_superuser +@if_auth @app.route('/lwp/users', methods=['POST', 'GET']) def lwp_users(): ''' returns users and get posts request : can edit or add user in page. this funtction uses sqlite3 ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - try: - trash = request.args.get('trash') - except KeyError: - trash = 0 - - su_users = query_db("SELECT COUNT(id) as num FROM users " - "WHERE su='Yes'", [], one=True) - - if request.args.get('token') == session.get('token') and \ - int(trash) == 1 and request.args.get('userid') and \ - request.args.get('username'): - nb_users = query_db("SELECT COUNT(id) as num FROM users", [], - one=True) - - if nb_users['num'] > 1: - if su_users['num'] <= 1: - su_user = query_db("SELECT username FROM users " - "WHERE su='Yes'", [], one=True) - - if su_user['username'] == request.args.get('username'): - flash(u'Can\'t delete the last admin user : %s' % - request.args.get('username'), 'error') - return redirect(url_for('lwp_users')) - - g.db.execute("DELETE FROM users WHERE id=? AND username=?", - [request.args.get('userid'), - request.args.get('username')]) - g.db.commit() - flash(u'Deleted %s' % request.args.get('username'), 'success') - return redirect(url_for('lwp_users')) - - flash(u'Can\'t delete the last user!', 'error') + try: + trash = request.args.get('trash') + except KeyError: + trash = 0 + + su_users = query_db("SELECT COUNT(id) as num FROM users " + "WHERE su='Yes'", [], one=True) + + if request.args.get('token') == session.get('token') and \ + int(trash) == 1 and request.args.get('userid') and \ + request.args.get('username'): + nb_users = query_db("SELECT COUNT(id) as num FROM users", [], + one=True) + + if nb_users['num'] > 1: + if su_users['num'] <= 1: + su_user = query_db("SELECT username FROM users " + "WHERE su='Yes'", [], one=True) + + if su_user['username'] == request.args.get('username'): + flash(u'Can\'t delete the last admin user : %s' % + request.args.get('username'), 'error') + return redirect(url_for('lwp_users')) + + g.db.execute("DELETE FROM users WHERE id=? AND username=?", + [request.args.get('userid'), + request.args.get('username')]) + g.db.commit() + flash(u'Deleted %s' % request.args.get('username'), 'success') return redirect(url_for('lwp_users')) - if request.method == 'POST': - users = query_db('SELECT id, name, username, su FROM users ' - 'ORDER BY id ASC') - - if request.form['newUser'] == 'True': - if not request.form['username'] in \ - [user['username'] for user in users]: - if re.match('^\w+$', request.form['username']) and \ - request.form['password1']: - if request.form['password1'] == \ - request.form['password2']: - if request.form['name']: - if re.match('[a-z A-Z0-9]{3,32}', - request.form['name']): - g.db.execute( - "INSERT INTO users " - "(name, username, password) " - "VALUES (?, ?, ?)", - [request.form['name'], - request.form['username'], - hash_passwd( - request.form['password1'])]) - g.db.commit() - else: - flash(u'Invalid name!', 'error') - else: - g.db.execute("INSERT INTO users " - "(username, password) VALUES " - "(?, ?)", - [request.form['username'], - hash_passwd( - request.form['password1'])]) - g.db.commit() - - flash(u'Created %s' % request.form['username'], - 'success') - else: - flash(u'No password match', 'error') - else: - flash(u'Invalid username or password!', 'error') - else: - flash(u'Username already exist!', 'error') + flash(u'Can\'t delete the last user!', 'error') + return redirect(url_for('lwp_users')) - elif request.form['newUser'] == 'False': - if request.form['password1'] == request.form['password2']: - if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): - if su_users['num'] <= 1: - su = 'Yes' + if request.method == 'POST': + users = query_db('SELECT id, name, username, su FROM users ' + 'ORDER BY id ASC') + + if request.form['newUser'] == 'True': + if not request.form['username'] in \ + [user['username'] for user in users]: + if re.match('^\w+$', request.form['username']) and \ + request.form['password1']: + if request.form['password1'] == \ + request.form['password2']: + if request.form['name']: + if re.match('[a-z A-Z0-9]{3,32}', + request.form['name']): + g.db.execute( + "INSERT INTO users " + "(name, username, password) " + "VALUES (?, ?, ?)", + [request.form['name'], + request.form['username'], + hash_passwd( + request.form['password1'])]) + g.db.commit() + else: + flash(u'Invalid name!', 'error') else: - try: - su = request.form['su'] - except KeyError: - su = 'No' - - if not request.form['name']: - g.db.execute("UPDATE users SET name='', su=? " - "WHERE username=?", - [su, request.form['username']]) - g.db.commit() - elif request.form['name'] and \ - not request.form['password1'] and \ - not request.form['password2']: - g.db.execute("UPDATE users SET name=?, su=? " - "WHERE username=?", - [request.form['name'], su, - request.form['username']]) - g.db.commit() - elif request.form['name'] and \ - request.form['password1'] and \ - request.form['password2']: - g.db.execute("UPDATE users SET " - "name=?, password=?, su=? WHERE " - "username=?", - [request.form['name'], + g.db.execute("INSERT INTO users " + "(username, password) VALUES " + "(?, ?)", + [request.form['username'], hash_passwd( - request.form['password1']), - su, request.form['username']]) - g.db.commit() - elif request.form['password1'] and \ - request.form['password2']: - g.db.execute("UPDATE users SET password=?, su=? " - "WHERE username=?", - [hash_passwd( - request.form['password1']), - su, request.form['username']]) + request.form['password1'])]) g.db.commit() - flash(u'Updated', 'success') + flash(u'Created %s' % request.form['username'], + 'success') else: - flash(u'Invalid name!', 'error') + flash(u'No password match', 'error') else: - flash(u'No password match', 'error') + flash(u'Invalid username or password!', 'error') else: - flash(u'Unknown error!', 'error') + flash(u'Username already exist!', 'error') - users = query_db("SELECT id, name, username, su FROM users " - "ORDER BY id ASC") - nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) - su_users = query_db("SELECT COUNT(id) as num FROM users " - "WHERE su='Yes'", [], one=True) + elif request.form['newUser'] == 'False': + if request.form['password1'] == request.form['password2']: + if re.match('[a-z A-Z0-9]{3,32}', request.form['name']): + if su_users['num'] <= 1: + su = 'Yes' + else: + try: + su = request.form['su'] + except KeyError: + su = 'No' + + if not request.form['name']: + g.db.execute("UPDATE users SET name='', su=? " + "WHERE username=?", + [su, request.form['username']]) + g.db.commit() + elif request.form['name'] and \ + not request.form['password1'] and \ + not request.form['password2']: + g.db.execute("UPDATE users SET name=?, su=? " + "WHERE username=?", + [request.form['name'], su, + request.form['username']]) + g.db.commit() + elif request.form['name'] and \ + request.form['password1'] and \ + request.form['password2']: + g.db.execute("UPDATE users SET " + "name=?, password=?, su=? WHERE " + "username=?", + [request.form['name'], + hash_passwd( + request.form['password1']), + su, request.form['username']]) + g.db.commit() + elif request.form['password1'] and \ + request.form['password2']: + g.db.execute("UPDATE users SET password=?, su=? " + "WHERE username=?", + [hash_passwd( + request.form['password1']), + su, request.form['username']]) + g.db.commit() + + flash(u'Updated', 'success') + else: + flash(u'Invalid name!', 'error') + else: + flash(u'No password match', 'error') + else: + flash(u'Unknown error!', 'error') - return render_template('users.html', containers=lxc.ls(), users=users, - nb_users=nb_users, su_users=su_users) - return render_template('login.html') + users = query_db("SELECT id, name, username, su FROM users " + "ORDER BY id ASC") + nb_users = query_db("SELECT COUNT(id) as num FROM users", [], one=True) + su_users = query_db("SELECT COUNT(id) as num FROM users " + "WHERE su='Yes'", [], one=True) + + return render_template('users.html', containers=lxc.ls(), users=users, + nb_users=nb_users, su_users=su_users) @app.route('/checkconfig') @@ -846,7 +836,7 @@ def get_token(): def query_db(query, args=(), one=False): cur = g.db.execute(query, args) rv = [dict((cur.description[idx][0], value) - for idx, value in enumerate(row)) for row in cur.fetchall()] + for idx, value in enumerate(row)) for row in cur.fetchall()] return (rv[0] if rv else None) if one else rv diff --git a/lwp/lxc/__init__.py b/lwp/lxc/__init__.py index 8218c2a..a117ffa 100644 --- a/lwp/lxc/__init__.py +++ b/lwp/lxc/__init__.py @@ -129,7 +129,7 @@ def memory_usage(name): universal_newlines=True).splitlines() except: return 0 - return int(out[0])/1024/1024 + return int(out[0]) / 1024 / 1024 def host_memory_usage(): @@ -156,10 +156,10 @@ def host_memory_usage(): cached = float(split[1]) out.close() used = (total - (free + buffers + cached)) - return {'percent': int((used/total)*100), - 'percent_cached': int(((cached)/total)*100), - 'used': int(used/1024), - 'total': int(total/1024)} + return {'percent': int((used / total) * 100), + 'percent_cached': int(((cached) / total) * 100), + 'used': int(used / 1024), + 'total': int(total / 1024)} def host_cpu_percent(): @@ -241,7 +241,7 @@ def get_templates_list(): if path: for line in path: - templates.append(line.replace('lxc-', '')) + templates.append(line.replace('lxc-', '')) return sorted(templates) @@ -270,13 +270,13 @@ def get_net_settings(): config = configparser.SafeConfigParser() cfg = {} config.readfp(FakeSection(open(filename))) - cfg['use'] = config.get('DEFAULT', 'USE_LXC_BRIDGE').strip('"') - cfg['bridge'] = config.get('DEFAULT', 'LXC_BRIDGE').strip('"') - cfg['address'] = config.get('DEFAULT', 'LXC_ADDR').strip('"') - cfg['netmask'] = config.get('DEFAULT', 'LXC_NETMASK').strip('"') - cfg['network'] = config.get('DEFAULT', 'LXC_NETWORK').strip('"') - cfg['range'] = config.get('DEFAULT', 'LXC_DHCP_RANGE').strip('"') - cfg['max'] = config.get('DEFAULT', 'LXC_DHCP_MAX').strip('"') + cfg['use'] = config.get('DEFAULT', 'USE_LXC_BRIDGE').strip('"').strip('"') + cfg['bridge'] = config.get('DEFAULT', 'LXC_BRIDGE').strip('"').strip('"') + cfg['address'] = config.get('DEFAULT', 'LXC_ADDR').strip('"').strip('"') + cfg['netmask'] = config.get('DEFAULT', 'LXC_NETMASK').strip('"').strip('"') + cfg['network'] = config.get('DEFAULT', 'LXC_NETWORK').strip('"').strip('"') + cfg['range'] = config.get('DEFAULT', 'LXC_DHCP_RANGE').strip('"').strip('"') + cfg['max'] = config.get('DEFAULT', 'LXC_DHCP_MAX').strip('"').strip('"') return cfg From bc6ead60b43c83cf50761ba26739947018c23644 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 09:08:26 +0300 Subject: [PATCH 10/20] [fix] add bin --- bin/lwp | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100755 bin/lwp diff --git a/bin/lwp b/bin/lwp new file mode 100755 index 0000000..ee462d6 --- /dev/null +++ b/bin/lwp @@ -0,0 +1,88 @@ +#!/usr/bin/env python +# encoding: utf-8 + +# LXC Python Library +# for compatibility with LXC 0.8 and 0.9 +# on Ubuntu 12.04/12.10/13.04 + +# Author: Elie Deloumeau +# Contact: elie@deloumeau.fr + +# The MIT License (MIT) +# Copyright (c) 2013 Elie Deloumeau + +# 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: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# 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. + +import argparse +import logging +from arconfig import GenConfigAction, LoadConfigAction +from uuid import uuid1 +import os + +parser = argparse.ArgumentParser("lwp") +parser.add_argument("--config", action=LoadConfigAction) +parser.add_argument("--gen-config", action=GenConfigAction) +parser.add_argument("--debug", default=False, action="store_true", help="Debug logging") + +group = parser.add_argument_group("database") +group.add_argument("-D", "--db-file", dest="db", + default="/var/lib/lxc/lwp.db", + help="Database file [default: /var/lib/lxc/lwp.db]") + +group = parser.add_argument_group("main") +group.add_argument("-l", "--address", default="0.0.0.0", help="Listen HTTP address [default: 0.0.0.0]", dest="address") +group.add_argument("-p", "--port", default=5000, help="Listen HTTP port [default: 5000]", type=int, dest="port") +group.add_argument("--session-timeout", default=600, type=int, dest="session_timeout") +group.add_argument("-S", "--secret", default=str(uuid1()), dest="secret") + + +from lwp.app import app + + +def main(host='0.0.0.0', port=5000): + log = logging.getLogger("lwp") + if not os.path.exists(app.options.directory): + log.fatal("LXC Directory doesn't exists") + return 128 + try: + import eventlet + from eventlet import wsgi + log.info("Starting through eventlet") + wsgi.server(eventlet.listen((host, port)), app) + except ImportError: + log.info("Starting through default WSGI engine") + app.run(host=host, port=port) + + return 0 + + +if __name__ == '__main__': + with app.app_context() as c: + app.options = parser.parse_args() + app.options.directory = '/var/lib/lxc' + app.config['SECRET_KEY'] = app.options.secret + + logging.basicConfig( + format=u'[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-6s %(message)s', + level=logging.INFO if app.options.debug else logging.DEBUG + ) + exit(main( + host=app.options.address, + port=app.options.port + )) From e55c3f6d51cdd23a5a1222fc70b5363f3333b548 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 09:11:32 +0300 Subject: [PATCH 11/20] [fix] fix syntax --- lwp/app/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lwp/app/views.py b/lwp/app/views.py index 79a600e..fd6b53a 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -308,8 +308,8 @@ def lxc_net(): '[01]?[0-9][0-9]?)' form = { - 'use': request.form.get('use', 'false') - 'bridge': request.form.get('bridge') + 'use': request.form.get('use', 'false'), + 'bridge': request.form.get('bridge'), 'address': request.form.get('address'), 'netmask': request.form.get('netmask'), 'network': request.form.get('network'), From 582dc2e3780950a1be16175eda817d2cab82a2c8 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 10:18:08 +0300 Subject: [PATCH 12/20] [fix] add caching --- lwp/app/views.py | 379 +++++++++++++++++++--------------------- lwp/lxc/__init__.py | 7 +- lwp/lxclite/__init__.py | 56 ++++-- setup.py | 2 +- 4 files changed, 229 insertions(+), 215 deletions(-) diff --git a/lwp/app/views.py b/lwp/app/views.py index fd6b53a..8f891ad 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -66,6 +66,7 @@ def teardown_request(exception): @app.route('/home') +@if_auth def home(): ''' home page function @@ -78,10 +79,13 @@ def home(): containers_by_status = [] for container in listx[status]: + settings = lwp.get_container_settings(container) + settings['ipv4'] = lxc.info(container).get('ip', settings.get('ipv4')) + containers_by_status.append({ 'name': container, 'memusg': lwp.memory_usage(container), - 'settings': lwp.get_container_settings(container) + 'settings': settings }) containers_all.append({ 'status': status.lower(), @@ -94,14 +98,14 @@ def home(): templates=lwp.get_templates_list()) -@if_auth @app.route('/') +@if_auth def index(): - return redirect(url_for(home), code=302) + return redirect(url_for('home'), code=302) -@if_auth @app.route('/about') +@if_auth def about(): ''' about page @@ -110,8 +114,8 @@ def about(): return render_template('about.html', containers=lxc.ls(), version=lwp.check_version()) -@if_auth @app.route('//edit', methods=['POST', 'GET']) +@if_auth def edit(container=None): ''' edit containers page and actions if form post request @@ -291,9 +295,9 @@ def edit(container=None): host_memory=host_memory) -@if_superuser -@if_auth @app.route('/settings/lxc-net', methods=['POST', 'GET']) +@if_auth +@if_superuser def lxc_net(): ''' lxc-net (/etc/default/lxc) settings page and actions if form post request @@ -363,9 +367,9 @@ def lxc_net(): running=lxc.running()) +@app.route('/lwp/users', methods=['POST', 'GET']) @if_superuser @if_auth -@app.route('/lwp/users', methods=['POST', 'GET']) def lwp_users(): ''' returns users and get posts request : can edit or add user in page. @@ -511,237 +515,227 @@ def lwp_users(): @app.route('/checkconfig') +@if_superuser def checkconfig(): ''' returns the display of lxc-checkconfig command ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - - return render_template('checkconfig.html', containers=lxc.ls(), - cfg=lxc.checkconfig()) - return render_template('login.html') + return render_template('checkconfig.html', containers=lxc.ls(), cfg=lxc.checkconfig()) @app.route('/action', methods=['GET']) +@if_auth def action(): ''' manage all actions related to containers lxc-start, lxc-stop, etc... ''' - if 'logged_in' in session: - if request.args['token'] == session.get('token'): - action = request.args['action'] - name = request.args['name'] + if request.args['token'] == session.get('token'): + action = request.args['action'] + name = request.args['name'] - if action == 'start': - try: - if lxc.start(name) == 0: - # Fix bug : "the container is randomly not - # displayed in overview list after a boot" - time.sleep(1) - flash(u'Container %s started successfully!' % name, - 'success') - else: - flash(u'Unable to start %s!' % name, 'error') - except lxc.ContainerAlreadyRunning: - flash(u'Container %s is already running!' % name, 'error') - elif action == 'stop': - try: - if lxc.stop(name) == 0: - flash(u'Container %s stopped successfully!' % name, - 'success') - else: - flash(u'Unable to stop %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s is already stopped!' % name, 'error') - elif action == 'freeze': - try: - if lxc.freeze(name) == 0: - flash(u'Container %s frozen successfully!' % name, - 'success') - else: - flash(u'Unable to freeze %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s not running!' % name, 'error') - elif action == 'unfreeze': - try: - if lxc.unfreeze(name) == 0: - flash(u'Container %s unfrozen successfully!' % name, - 'success') - else: - flash(u'Unable to unfeeze %s!' % name, 'error') - except lxc.ContainerNotRunning: - flash(u'Container %s not frozen!' % name, 'error') - elif action == 'destroy': - if session['su'] != 'Yes': - return abort(403) - try: - if lxc.destroy(name) == 0: - flash(u'Container %s destroyed successfully!' % name, - 'success') - else: - flash(u'Unable to destroy %s!' % name, 'error') - except lxc.ContainerDoesntExists: - flash(u'The Container %s does not exists!' % name, 'error') - elif action == 'reboot' and name == 'host': - if session['su'] != 'Yes': - return abort(403) - msg = '\v*** LXC Web Panel *** \ - \nReboot from web panel' - try: - subprocess.check_call('/sbin/shutdown -r now \'%s\'' % msg, - shell=True) - flash(u'System will now restart!', 'success') - except: - flash(u'System error!', 'error') - try: - if request.args['from'] == 'edit': - return redirect('../%s/edit' % name) - else: - return redirect(url_for('home')) - except: + if action == 'start': + try: + if lxc.start(name) == 0: + # Fix bug : "the container is randomly not + # displayed in overview list after a boot" + time.sleep(1) + flash(u'Container %s started successfully!' % name, + 'success') + else: + flash(u'Unable to start %s!' % name, 'error') + except lxc.ContainerAlreadyRunning: + flash(u'Container %s is already running!' % name, 'error') + elif action == 'stop': + try: + if lxc.stop(name) == 0: + flash(u'Container %s stopped successfully!' % name, + 'success') + else: + flash(u'Unable to stop %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s is already stopped!' % name, 'error') + elif action == 'freeze': + try: + if lxc.freeze(name) == 0: + flash(u'Container %s frozen successfully!' % name, + 'success') + else: + flash(u'Unable to freeze %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s not running!' % name, 'error') + elif action == 'unfreeze': + try: + if lxc.unfreeze(name) == 0: + flash(u'Container %s unfrozen successfully!' % name, + 'success') + else: + flash(u'Unable to unfeeze %s!' % name, 'error') + except lxc.ContainerNotRunning: + flash(u'Container %s not frozen!' % name, 'error') + elif action == 'destroy': + if session['su'] != 'Yes': + return abort(403) + try: + if lxc.destroy(name) == 0: + flash(u'Container %s destroyed successfully!' % name, + 'success') + else: + flash(u'Unable to destroy %s!' % name, 'error') + except lxc.ContainerDoesntExists: + flash(u'The Container %s does not exists!' % name, 'error') + elif action == 'reboot' and name == 'host': + if session['su'] != 'Yes': + return abort(403) + msg = '\v*** LXC Web Panel *** \ + \nReboot from web panel' + try: + subprocess.check_call('/sbin/shutdown -r now \'%s\'' % msg, + shell=True) + flash(u'System will now restart!', 'success') + except: + flash(u'System error!', 'error') + try: + if request.args['from'] == 'edit': + return redirect('../%s/edit' % name) + else: return redirect(url_for('home')) - return render_template('login.html') + except: + return redirect(url_for('home')) @app.route('/action/create-container', methods=['GET', 'POST']) +@if_auth +@if_superuser def create_container(): ''' verify all forms to create a container ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - if request.method == 'POST': - name = request.form['name'] - template = request.form['template'] - command = request.form['command'] - - if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): - storage_method = request.form['backingstore'] + if request.method == 'POST': + name = request.form['name'] + template = request.form['template'] + command = request.form['command'] - if storage_method == 'default': - try: - if lxc.create(name, template=template, - xargs=command) == 0: - flash(u'Container %s created successfully!' % name, - 'success') - else: - flash(u'Failed to create %s!' % name, 'error') - except lxc.ContainerAlreadyExists: - flash(u'The Container %s is already created!' % name, - 'error') - except subprocess.CalledProcessError: - flash(u'Error!' % name, 'error') + if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): + storage_method = request.form['backingstore'] - elif storage_method == 'directory': - directory = request.form['dir'] + if storage_method == 'default': + try: + if lxc.create(name, template=template, + xargs=command) == 0: + flash(u'Container %s created successfully!' % name, + 'success') + else: + flash(u'Failed to create %s!' % name, 'error') + except lxc.ContainerAlreadyExists: + flash(u'The Container %s is already created!' % name, + 'error') + except subprocess.CalledProcessError: + flash(u'Error!' % name, 'error') - if re.match('^/[a-zA-Z0-9_/-]+$', directory) and \ - directory != '': - try: - if lxc.create(name, template=template, - storage='dir --dir %s' % directory, - xargs=command) == 0: - flash(u'Container %s created successfully!' - % name, 'success') - else: - flash(u'Failed to create %s!' % name, 'error') - except lxc.ContainerAlreadyExists: - flash(u'The Container %s is already created!' - % name, 'error') - except subprocess.CalledProcessError: - flash(u'Error!' % name, 'error') - - elif storage_method == 'lvm': - lvname = request.form['lvname'] - vgname = request.form['vgname'] - fstype = request.form['fstype'] - fssize = request.form['fssize'] - storage_options = 'lvm' - - if re.match('^[a-zA-Z0-9_-]+$', lvname) and lvname != '': - storage_options += ' --lvname %s' % lvname - if re.match('^[a-zA-Z0-9_-]+$', vgname) and vgname != '': - storage_options += ' --vgname %s' % vgname - if re.match('^[a-z0-9]+$', fstype) and fstype != '': - storage_options += ' --fstype %s' % fstype - if re.match('^[0-9][G|M]$', fssize) and fssize != '': - storage_options += ' --fssize %s' % fssize + elif storage_method == 'directory': + directory = request.form['dir'] + if re.match('^/[a-zA-Z0-9_/-]+$', directory) and \ + directory != '': try: if lxc.create(name, template=template, - storage=storage_options, + storage='dir --dir %s' % directory, xargs=command) == 0: - flash(u'Container %s created successfully!' % name, - 'success') + flash(u'Container %s created successfully!' + % name, 'success') else: flash(u'Failed to create %s!' % name, 'error') except lxc.ContainerAlreadyExists: - flash(u'The container/logical volume %s is ' - 'already created!' % name, 'error') + flash(u'The Container %s is already created!' + % name, 'error') except subprocess.CalledProcessError: flash(u'Error!' % name, 'error') - else: - flash(u'Missing parameters to create container!', 'error') + elif storage_method == 'lvm': + lvname = request.form['lvname'] + vgname = request.form['vgname'] + fstype = request.form['fstype'] + fssize = request.form['fssize'] + storage_options = 'lvm' + + if re.match('^[a-zA-Z0-9_-]+$', lvname) and lvname != '': + storage_options += ' --lvname %s' % lvname + if re.match('^[a-zA-Z0-9_-]+$', vgname) and vgname != '': + storage_options += ' --vgname %s' % vgname + if re.match('^[a-z0-9]+$', fstype) and fstype != '': + storage_options += ' --fstype %s' % fstype + if re.match('^[0-9][G|M]$', fssize) and fssize != '': + storage_options += ' --fssize %s' % fssize + + try: + if lxc.create(name, template=template, + storage=storage_options, + xargs=command) == 0: + flash(u'Container %s created successfully!' % name, + 'success') + else: + flash(u'Failed to create %s!' % name, 'error') + except lxc.ContainerAlreadyExists: + flash(u'The container/logical volume %s is ' + 'already created!' % name, 'error') + except subprocess.CalledProcessError: + flash(u'Error!' % name, 'error') else: - if name == '': - flash(u'Please enter a container name!', 'error') - else: - flash(u'Invalid name for \"%s\"!' % name, 'error') + flash(u'Missing parameters to create container!', 'error') - return redirect(url_for('home')) - return render_template('login.html') + else: + if name == '': + flash(u'Please enter a container name!', 'error') + else: + flash(u'Invalid name for \"%s\"!' % name, 'error') + + return redirect(url_for('home')) @app.route('/action/clone-container', methods=['GET', 'POST']) +@if_auth +@if_superuser def clone_container(): ''' verify all forms to clone a container ''' - if 'logged_in' in session: - if session['su'] != 'Yes': - return abort(403) - if request.method == 'POST': - orig = request.form['orig'] - name = request.form['name'] - - try: - snapshot = request.form['snapshot'] - if snapshot == 'True': - snapshot = True - except KeyError: - snapshot = False + if request.method == 'POST': + orig = request.form['orig'] + name = request.form['name'] - if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): - out = None + try: + snapshot = request.form['snapshot'] + if snapshot == 'True': + snapshot = True + except KeyError: + snapshot = False - try: - out = lxc.clone(orig=orig, new=name, snapshot=snapshot) - except lxc.ContainerAlreadyExists: - flash(u'The Container %s already exists!' % name, 'error') - except subprocess.CalledProcessError: - flash(u'Can\'t snapshot a directory', 'error') + if re.match('^(?!^containers$)|[a-zA-Z0-9_-]+$', name): + out = None - if out and out == 0: - flash(u'Container %s cloned into %s successfully!' - % (orig, name), 'success') - elif out and out != 0: - flash(u'Failed to clone %s into %s!' % (orig, name), - 'error') + try: + out = lxc.clone(orig=orig, new=name, snapshot=snapshot) + except lxc.ContainerAlreadyExists: + flash(u'The Container %s already exists!' % name, 'error') + except subprocess.CalledProcessError: + flash(u'Can\'t snapshot a directory', 'error') + + if out and out == 0: + flash(u'Container %s cloned into %s successfully!' + % (orig, name), 'success') + elif out and out != 0: + flash(u'Failed to clone %s into %s!' % (orig, name), + 'error') + else: + if name == '': + flash(u'Please enter a container name!', 'error') else: - if name == '': - flash(u'Please enter a container name!', 'error') - else: - flash(u'Invalid name for \"%s\"!' % name, 'error') + flash(u'Invalid name for \"%s\"!' % name, 'error') - return redirect(url_for('home')) - return render_template('login.html') + return redirect(url_for('home')) @app.route('/login', methods=['GET', 'POST']) @@ -775,12 +769,7 @@ def login(): @app.route('/logout') def logout(): - session.pop('logged_in', None) - session.pop('token', None) - session.pop('last_activity', None) - session.pop('username', None) - session.pop('name', None) - session.pop('su', None) + session.clear() flash(u'You are logged out!', 'success') return redirect(url_for('login')) diff --git a/lwp/lxc/__init__.py b/lwp/lxc/__init__.py index a117ffa..7ebe414 100644 --- a/lwp/lxc/__init__.py +++ b/lwp/lxc/__init__.py @@ -250,12 +250,7 @@ def check_version(): ''' returns latest LWP version (dict with current and latest) ''' - f = open('version') - current = float(f.read()) - f.close() - latest = float(urlopen('http://lxc-webpanel.github.com/version').read()) - return {'current': current, - 'latest': latest} + return {'current': None } def get_net_settings(): diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index 1e01f50..edb1460 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -26,10 +26,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +from object_cacher import ObjectCacher import subprocess import os # TODO: Use it everywhere. + + def _run(cmd, output=False): ''' To run command easier @@ -97,6 +100,9 @@ def create(container, template='ubuntu', storage=None, xargs=None): if xargs: command += ' -- {}'.format(xargs) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run(command) @@ -114,9 +120,13 @@ def clone(orig=None, new=None, snapshot=False): if snapshot: command += ' -s' + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run(command) +@ObjectCacher(iod="lxc.info") def info(container): ''' Check info from lxc-info @@ -126,24 +136,25 @@ def info(container): raise ContainerDoesntExists( 'Container {} does not exist!'.format(container)) - output = _run('lxc-info -qn {}|grep -i "State\|PID"'.format(container), + output = _run('lxc-info -qn {}|grep -i "State\|PID\|IP"'.format(container), output=True) - if output: - output = output.splitlines() - else: - return {"state": None, "pid": None} - state = output[0].split()[1] + params = {"state": None, "pid": None} + if output: + for i in output.splitlines(): + n, v = i.split() + n = n.strip(":").lower() + params[n] = v - if state == 'STOPPED': - pid = "0" - else: - pid = output[1].split()[1] + try: + params['pid'] = int(params['pid']) + except: + pass - return {'state': state, - 'pid': pid} + return params +@ObjectCacher(iod="lxc.list") def ls(): ''' List containers directory @@ -158,7 +169,7 @@ def ls(): try: ct_list = [x for x in os.listdir(base_path) - if os.path.isdir(os.path.join(base_path, x))] + if os.path.isdir(os.path.join(base_path, x)) and os.path.exists(os.path.join(base_path, x, 'config'))] except OSError: ct_list = [] @@ -214,6 +225,9 @@ def start(container): raise ContainerAlreadyRunning( 'Container {} is already running!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-start -dn {}'.format(container)) @@ -230,6 +244,9 @@ def stop(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-stop -n {}'.format(container)) @@ -246,6 +263,9 @@ def freeze(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-freeze -n {}'.format(container)) @@ -262,6 +282,9 @@ def unfreeze(container): raise ContainerNotRunning( 'Container {} is not frozen!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-unfreeze -n {}'.format(container)) @@ -274,9 +297,13 @@ def destroy(container): raise ContainerDoesntExists( 'Container {} does not exists!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-destroy -n {}'.format(container)) +@ObjectCacher(iod="lxc.list", timeout=5) def checkconfig(): ''' Returns the output of lxc-checkconfig (colors cleared) @@ -297,4 +324,7 @@ def cgroup(container, key, value): raise ContainerDoesntExists( 'Container {} does not exist!'.format(container)) + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + return _run('lxc-cgroup -n {} {} {}'.format(container, key, value)) diff --git a/setup.py b/setup.py index 933a77a..5f340de 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ 'install_requires': [ 'flask==0.9', 'arconfig', + 'object_cacher', ] } @@ -42,4 +43,3 @@ packages=find_packages(), **supports ) - From 097900105d67f3dc8f7c44d17c0d477c73c572d5 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 10:21:18 +0300 Subject: [PATCH 13/20] [fix] add caching --- lwp/app/views.py | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/lwp/app/views.py b/lwp/app/views.py index 8f891ad..0b64b61 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -775,43 +775,48 @@ def logout(): @app.route('/_refresh_cpu_host') +@if_auth +@ObjectCacher(timeout=5) def refresh_cpu_host(): - if 'logged_in' in session: - return lwp.host_cpu_percent() + return lwp.host_cpu_percent() @app.route('/_refresh_uptime_host') +@if_auth +@ObjectCacher(timeout=5) def refresh_uptime_host(): - if 'logged_in' in session: - return jsonify(lwp.host_uptime()) + return jsonify(lwp.host_uptime()) @app.route('/_refresh_disk_host') +@if_auth +@ObjectCacher(timeout=5) def refresh_disk_host(): - if 'logged_in' in session: - return jsonify(lwp.host_disk_usage(directory=app.options.directory)) + return jsonify(lwp.host_disk_usage(directory=app.options.directory)) @app.route('/_refresh_memory_') +@if_auth +@ObjectCacher(timeout=5) def refresh_memory_containers(name=None): - if 'logged_in' in session: - if name == 'containers': - containers_running = lxc.running() - containers = [] - for container in containers_running: - container = container.replace(' (auto)', '') - containers.append({'name': container, - 'memusg': lwp.memory_usage(container)}) - return jsonify(data=containers) - elif name == 'host': - return jsonify(lwp.host_memory_usage()) - return jsonify({'memusg': lwp.memory_usage(name)}) + if name == 'containers': + containers_running = lxc.running() + containers = [] + for container in containers_running: + container = container.replace(' (auto)', '') + containers.append({'name': container, + 'memusg': lwp.memory_usage(container)}) + return jsonify(data=containers) + elif name == 'host': + return jsonify(lwp.host_memory_usage()) + return jsonify({'memusg': lwp.memory_usage(name)}) @app.route('/_check_version') +@if_auth +@ObjectCacher(timeout=5) def check_version(): - if 'logged_in' in session: - return jsonify(lwp.check_version()) + return jsonify(lwp.check_version()) def hash_passwd(passwd): From 0811820fb406802b5e74594c0c94e9484cc2c173 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 12:46:38 +0300 Subject: [PATCH 14/20] [fixes] --- lwp/app/views.py | 4 +++- lwp/lxclite/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lwp/app/views.py b/lwp/app/views.py index 0b64b61..ff7ee52 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -12,6 +12,8 @@ import sqlite3 import os +from object_cacher import ObjectCacher + from flask import request, session, g, redirect, url_for, abort, render_template, flash, jsonify from . import app @@ -814,7 +816,7 @@ def refresh_memory_containers(name=None): @app.route('/_check_version') @if_auth -@ObjectCacher(timeout=5) +@ObjectCacher(timeout=3600) def check_version(): return jsonify(lwp.check_version()) diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index edb1460..7388610 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -126,7 +126,7 @@ def clone(orig=None, new=None, snapshot=False): return _run(command) -@ObjectCacher(iod="lxc.info") +@ObjectCacher(oid="lxc.info") def info(container): ''' Check info from lxc-info @@ -154,7 +154,7 @@ def info(container): return params -@ObjectCacher(iod="lxc.list") +@ObjectCacher(oid="lxc.list") def ls(): ''' List containers directory @@ -303,7 +303,7 @@ def destroy(container): return _run('lxc-destroy -n {}'.format(container)) -@ObjectCacher(iod="lxc.list", timeout=5) +@ObjectCacher(oid="lxc.list", timeout=5) def checkconfig(): ''' Returns the output of lxc-checkconfig (colors cleared) From 406c13d25a0c5d7985f5b9ccff022875b8587b01 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 13:33:08 +0300 Subject: [PATCH 15/20] [fix] add cache --- bin/lwp | 15 +++++++++------ lwp/app/views.py | 2 +- lwp/lxclite/__init__.py | 2 +- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/bin/lwp b/bin/lwp index ee462d6..6194dc3 100755 --- a/bin/lwp +++ b/bin/lwp @@ -38,7 +38,7 @@ import os parser = argparse.ArgumentParser("lwp") parser.add_argument("--config", action=LoadConfigAction) parser.add_argument("--gen-config", action=GenConfigAction) -parser.add_argument("--debug", default=False, action="store_true", help="Debug logging") +parser.add_argument("--debug", default=False, action="store_true", help="Debugging output") group = parser.add_argument_group("database") group.add_argument("-D", "--db-file", dest="db", @@ -51,6 +51,13 @@ group.add_argument("-p", "--port", default=5000, help="Listen HTTP port [default group.add_argument("--session-timeout", default=600, type=int, dest="session_timeout") group.add_argument("-S", "--secret", default=str(uuid1()), dest="secret") +if __name__ == '__main__': + options = parser.parse_args() + logging.basicConfig( + format=u'[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-6s %(message)s', + level=logging.INFO if options.debug else logging.DEBUG + ) + from lwp.app import app @@ -74,14 +81,10 @@ def main(host='0.0.0.0', port=5000): if __name__ == '__main__': with app.app_context() as c: - app.options = parser.parse_args() + app.options = options app.options.directory = '/var/lib/lxc' app.config['SECRET_KEY'] = app.options.secret - logging.basicConfig( - format=u'[%(asctime)s] %(filename)s:%(lineno)d %(levelname)-6s %(message)s', - level=logging.INFO if app.options.debug else logging.DEBUG - ) exit(main( host=app.options.address, port=app.options.port diff --git a/lwp/app/views.py b/lwp/app/views.py index ff7ee52..b68be44 100644 --- a/lwp/app/views.py +++ b/lwp/app/views.py @@ -34,7 +34,7 @@ def wrap(*args, **kwargs): if 'logged_in' in session: return func(*args, **kwargs) else: - return render_template('login.html') + return render_template('login.html'), 403 return wrap diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index 7388610..9f517ac 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -303,7 +303,7 @@ def destroy(container): return _run('lxc-destroy -n {}'.format(container)) -@ObjectCacher(oid="lxc.list", timeout=5) +@ObjectCacher(oid="lxc.chkconfig", timeout=5) def checkconfig(): ''' Returns the output of lxc-checkconfig (colors cleared) From 5d098e67184172d064b48e200d71ed1d4643a683 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 13:33:34 +0300 Subject: [PATCH 16/20] [fix] bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f340de..d3d545b 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from distutils.core import setup -__version__ = '0.2-pre0' +__version__ = '0.2-pre1' __author__ = 'Élie Deloumeau, Antoine Tanzilli' From 31b43f0553380b6928021515fa24e147f10013f7 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 14:08:02 +0300 Subject: [PATCH 17/20] [fix] bump --- lwp/lxclite/__init__.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index 9f517ac..6f4d201 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -126,7 +126,7 @@ def clone(orig=None, new=None, snapshot=False): return _run(command) -@ObjectCacher(oid="lxc.info") +@ObjectCacher(oid="lxc.info", timeout=5) def info(container): ''' Check info from lxc-info @@ -154,7 +154,7 @@ def info(container): return params -@ObjectCacher(oid="lxc.list") +@ObjectCacher(oid="lxc.list", timeout=20) def ls(): ''' List containers directory diff --git a/setup.py b/setup.py index d3d545b..f1814b4 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from distutils.core import setup -__version__ = '0.2-pre1' +__version__ = '0.2-pre2' __author__ = 'Élie Deloumeau, Antoine Tanzilli' From d34624437c91d4443b96d01006ea02dfe11ff81d Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 14:11:22 +0300 Subject: [PATCH 18/20] [fix] fix invalidate error --- lwp/lxclite/__init__.py | 59 ++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 16 deletions(-) diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index 6f4d201..e5bd2df 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -29,6 +29,9 @@ from object_cacher import ObjectCacher import subprocess import os +import logging + +log = logging.getLogger("lxclite") # TODO: Use it everywhere. @@ -100,8 +103,11 @@ def create(container, template='ubuntu', storage=None, xargs=None): if xargs: command += ' -- {}'.format(xargs) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run(command) @@ -120,8 +126,11 @@ def clone(orig=None, new=None, snapshot=False): if snapshot: command += ' -s' - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run(command) @@ -225,8 +234,11 @@ def start(container): raise ContainerAlreadyRunning( 'Container {} is already running!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-start -dn {}'.format(container)) @@ -244,8 +256,11 @@ def stop(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-stop -n {}'.format(container)) @@ -263,8 +278,11 @@ def freeze(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-freeze -n {}'.format(container)) @@ -282,8 +300,11 @@ def unfreeze(container): raise ContainerNotRunning( 'Container {} is not frozen!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-unfreeze -n {}'.format(container)) @@ -297,8 +318,11 @@ def destroy(container): raise ContainerDoesntExists( 'Container {} does not exists!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-destroy -n {}'.format(container)) @@ -324,7 +348,10 @@ def cgroup(container, key, value): raise ContainerDoesntExists( 'Container {} does not exist!'.format(container)) - ObjectCacher.invalidate("lxc.info") - ObjectCacher.invalidate("lxc.list") + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) return _run('lxc-cgroup -n {} {} {}'.format(container, key, value)) From b4f33108c96bf6678caba571576e9380b9d0887a Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 14:11:45 +0300 Subject: [PATCH 19/20] [fix] bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f1814b4..73f099a 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from distutils.core import setup -__version__ = '0.2-pre2' +__version__ = '0.2-pre3' __author__ = 'Élie Deloumeau, Antoine Tanzilli' From a14dbddbe9dc8031c42d0cdd66a2298dcf968a59 Mon Sep 17 00:00:00 2001 From: Dmitry Orlov Date: Thu, 4 Dec 2014 14:17:10 +0300 Subject: [PATCH 20/20] [fix] fix returncode --- lwp/lxclite/__init__.py | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lwp/lxclite/__init__.py b/lwp/lxclite/__init__.py index e5bd2df..803c286 100644 --- a/lwp/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -52,9 +52,9 @@ def _run(cmd, output=False): try: subprocess.check_call('{}'.format(cmd), shell=True, universal_newlines=True) # returns 0 for True - return True - except subprocess.CalledProcessError: - return False + return 0 + except subprocess.CalledProcessError as e: + return e.returncode class ContainerAlreadyExists(Exception): diff --git a/setup.py b/setup.py index 73f099a..d7eea44 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ from distutils.core import setup -__version__ = '0.2-pre3' +__version__ = '0.2-pre4' __author__ = 'Élie Deloumeau, Antoine Tanzilli'