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 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/bin/lwp b/bin/lwp new file mode 100755 index 0000000..6194dc3 --- /dev/null +++ b/bin/lwp @@ -0,0 +1,91 @@ +#!/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="Debugging output") + +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") + +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 + + +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 = options + app.options.directory = '/var/lib/lxc' + app.config['SECRET_KEY'] = app.options.secret + + exit(main( + host=app.options.address, + port=app.options.port + )) diff --git a/lwp.conf b/lwp.conf deleted file mode 100644 index d0a99d2..0000000 --- a/lwp.conf +++ /dev/null @@ -1,13 +0,0 @@ -[global] -address = 0.0.0.0 -port = 5000 -debug = False - -[database] -file = lwp.db - -[session] -time = 10 - -[overview] -partition = / 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/__init__.py b/lwp/__init__.py index 9ba9a76..bca5f67 100644 --- a/lwp/__init__.py +++ b/lwp/__init__.py @@ -1,466 +1 @@ -# 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 sys -sys.path.append('../') -from lxclite import exists, stopped, ContainerDoesntExists - -import os -import platform -import re -import subprocess -import time - -from io import StringIO - -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen - -try: - import configparser -except ImportError: - import ConfigParser as configparser - - -class CalledProcessError(Exception): - pass - -cgroup = {} -cgroup['type'] = 'lxc.network.type' -cgroup['link'] = 'lxc.network.link' -cgroup['flags'] = 'lxc.network.flags' -cgroup['hwaddr'] = 'lxc.network.hwaddr' -cgroup['rootfs'] = 'lxc.rootfs' -cgroup['utsname'] = 'lxc.utsname' -cgroup['arch'] = 'lxc.arch' -cgroup['ipv4'] = 'lxc.network.ipv4' -cgroup['memlimit'] = 'lxc.cgroup.memory.limit_in_bytes' -cgroup['swlimit'] = 'lxc.cgroup.memory.memsw.limit_in_bytes' -cgroup['cpus'] = 'lxc.cgroup.cpuset.cpus' -cgroup['shares'] = 'lxc.cgroup.cpu.shares' -cgroup['deny'] = 'lxc.cgroup.devices.deny' -cgroup['allow'] = 'lxc.cgroup.devices.allow' - - -def FakeSection(fp): - content = u"[DEFAULT]\n%s" % fp.read() - - return StringIO(content) - - -def DelSection(filename=None): - if filename: - load = open(filename, 'r') - read = load.readlines() - load.close() - i = 0 - while i < len(read): - if '[DEFAULT]' in read[i]: - del read[i] - break - load = open(filename, 'w') - load.writelines(read) - load.close() - - -def file_exist(filename): - ''' - checks if a given file exist or not - ''' - try: - with open(filename) as f: - f.close() - return True - except IOError: - return False - - -def ls_auto(): - ''' - returns a list of autostart containers - ''' - try: - auto_list = os.listdir('/etc/lxc/auto/') - except OSError: - auto_list = [] - return auto_list - - -def memory_usage(name): - ''' - returns memory usage in MB - ''' - if not exists(name): - raise ContainerDoesntExists( - "The container (%s) does not exist!" % name) - - if name in stopped(): - return 0 - - cmd = ['lxc-cgroup -n %s memory.usage_in_bytes' % name] - try: - out = subprocess.check_output(cmd, shell=True, - universal_newlines=True).splitlines() - except: - return 0 - return int(out[0])/1024/1024 - - -def host_memory_usage(): - ''' - returns a dict of host memory usage values - {'percent': int((used/total)*100), - 'percent_cached':int((cached/total)*100), - 'used': int(used/1024), - 'total': int(total/1024)} - ''' - out = open('/proc/meminfo') - for line in out: - if 'MemTotal:' == line.split()[0]: - split = line.split() - total = float(split[1]) - if 'MemFree:' == line.split()[0]: - split = line.split() - free = float(split[1]) - if 'Buffers:' == line.split()[0]: - split = line.split() - buffers = float(split[1]) - if 'Cached:' == line.split()[0]: - split = line.split() - 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)} - - -def host_cpu_percent(): - ''' - returns CPU usage in percent - ''' - f = open('/proc/stat', 'r') - line = f.readlines()[0] - data = line.split() - previdle = float(data[4]) - prevtotal = float(data[1]) + float(data[2]) + \ - float(data[3]) + float(data[4]) - f.close() - time.sleep(0.1) - f = open('/proc/stat', 'r') - line = f.readlines()[0] - data = line.split() - idle = float(data[4]) - total = float(data[1]) + float(data[2]) + float(data[3]) + float(data[4]) - f.close() - intervaltotal = total - prevtotal - percent = 100 * (intervaltotal - (idle - previdle)) / intervaltotal - return str('%.1f' % percent) - - -def host_disk_usage(partition=None): - ''' - returns a dict of disk usage values - {'total': usage[1], - 'used': usage[2], - 'free': usage[3], - 'percent': usage[4]} - ''' - if not partition: - partition = '/' - - usage = subprocess.check_output(['df -h %s' % partition], - universal_newlines=True, - shell=True).split('\n')[1].split() - return {'total': usage[1], - 'used': usage[2], - 'free': usage[3], - 'percent': usage[4]} - - -def host_uptime(): - ''' - returns a dict of the system uptime - {'day': days, - 'time': '%d:%02d' % (hours,minutes)} - ''' - f = open('/proc/uptime') - uptime = int(f.readlines()[0].split('.')[0]) - minutes = uptime / 60 % 60 - hours = uptime / 60 / 60 % 24 - days = uptime / 60 / 60 / 24 - f.close() - return {'day': days, - 'time': '%d:%02d' % (hours, minutes)} - - -def check_ubuntu(): - ''' - return the System version - ''' - dist = '%s %s' % (platform.linux_distribution()[0], - platform.linux_distribution()[1]) - return dist - - -def get_templates_list(): - ''' - returns a sorted lxc templates list - ''' - templates = [] - path = None - - try: - path = os.listdir('/usr/share/lxc/templates') - except: - path = os.listdir('/usr/lib/lxc/templates') - - if path: - for line in path: - templates.append(line.replace('lxc-', '')) - - return sorted(templates) - - -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} - - -def get_net_settings(): - ''' - returns a dict of all known settings for LXC networking - ''' - filename = '/etc/default/lxc-net' - if not file_exist(filename): - filename = '/etc/default/lxc' - if not file_exist(filename): - return False - 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('"') - return cfg - - -def get_container_settings(name): - ''' - returns a dict of all utils settings for a container - ''' - - if os.geteuid(): - filename = os.path.expanduser('~/.local/share/lxc/%s/config' % name) - else: - filename = '/var/lib/lxc/%s/config' % name - - if not file_exist(filename): - return False - config = configparser.SafeConfigParser() - cfg = {} - config.readfp(FakeSection(open(filename))) - try: - cfg['type'] = config.get('DEFAULT', cgroup['type']) - except configparser.NoOptionError: - cfg['type'] = '' - try: - cfg['link'] = config.get('DEFAULT', cgroup['link']) - except configparser.NoOptionError: - cfg['link'] = '' - try: - cfg['flags'] = config.get('DEFAULT', cgroup['flags']) - except configparser.NoOptionError: - cfg['flags'] = '' - try: - cfg['hwaddr'] = config.get('DEFAULT', cgroup['hwaddr']) - except configparser.NoOptionError: - cfg['hwaddr'] = '' - try: - cfg['rootfs'] = config.get('DEFAULT', cgroup['rootfs']) - except configparser.NoOptionError: - cfg['rootfs'] = '' - try: - cfg['utsname'] = config.get('DEFAULT', cgroup['utsname']) - except configparser.NoOptionError: - cfg['utsname'] = '' - try: - cfg['arch'] = config.get('DEFAULT', cgroup['arch']) - except configparser.NoOptionError: - cfg['arch'] = '' - try: - cfg['ipv4'] = config.get('DEFAULT', cgroup['ipv4']) - except configparser.NoOptionError: - cfg['ipv4'] = '' - try: - cfg['memlimit'] = re.sub(r'[a-zA-Z]', '', - config.get('DEFAULT', cgroup['memlimit'])) - except configparser.NoOptionError: - cfg['memlimit'] = '' - try: - cfg['swlimit'] = re.sub(r'[a-zA-Z]', '', - config.get('DEFAULT', cgroup['swlimit'])) - except configparser.NoOptionError: - cfg['swlimit'] = '' - try: - cfg['cpus'] = config.get('DEFAULT', cgroup['cpus']) - except configparser.NoOptionError: - cfg['cpus'] = '' - try: - cfg['shares'] = config.get('DEFAULT', cgroup['shares']) - except configparser.NoOptionError: - cfg['shares'] = '' - - if '%s.conf' % name in ls_auto(): - cfg['auto'] = True - else: - cfg['auto'] = False - - return cfg - - -def push_net_value(key, value, filename='/etc/default/lxc'): - ''' - replace a var in the lxc-net config file - ''' - if filename: - config = configparser.RawConfigParser() - config.readfp(FakeSection(open(filename))) - if not value: - config.remove_option('DEFAULT', key) - else: - config.set('DEFAULT', key, value) - - with open(filename, 'wb') as configfile: - config.write(configfile) - - DelSection(filename=filename) - - load = open(filename, 'r') - read = load.readlines() - load.close() - i = 0 - while i < len(read): - if ' = ' in read[i]: - split = read[i].split(' = ') - split[1] = split[1].strip('\n') - if '\"' in split[1]: - read[i] = '%s=%s\n' % (split[0].upper(), split[1]) - else: - read[i] = '%s=\"%s\"\n' % (split[0].upper(), split[1]) - i += 1 - load = open(filename, 'w') - load.writelines(read) - load.close() - - -def push_config_value(key, value, container=None): - ''' - replace a var in a container config file - ''' - - def save_cgroup_devices(filename=None): - ''' - returns multiple values (lxc.cgroup.devices.deny and - lxc.cgroup.devices.allow) in a list because configparser cannot - make this... - ''' - if filename: - values = [] - i = 0 - - load = open(filename, 'r') - read = load.readlines() - load.close() - - while i < len(read): - if not read[i].startswith('#') and \ - re.match('lxc.cgroup.devices.deny|' - 'lxc.cgroup.devices.allow', read[i]): - values.append(read[i]) - i += 1 - return values - - if container: - if os.geteuid(): - filename = os.path.expanduser('~/.local/share/lxc/%s/config' % - container) - else: - filename = '/var/lib/lxc/%s/config' % container - - save = save_cgroup_devices(filename=filename) - - config = configparser.RawConfigParser() - config.readfp(FakeSection(open(filename))) - if not value: - config.remove_option('DEFAULT', key) - elif key == cgroup['memlimit'] or key == cgroup['swlimit'] \ - and value is not False: - config.set('DEFAULT', key, '%sM' % value) - else: - config.set('DEFAULT', key, value) - - # Bugfix (can't duplicate keys with config parser) - if config.has_option('DEFAULT', cgroup['deny']) or \ - config.has_option('DEFAULT', cgroup['allow']): - config.remove_option('DEFAULT', cgroup['deny']) - config.remove_option('DEFAULT', cgroup['allow']) - - with open(filename, 'wb') as configfile: - config.write(configfile) - - DelSection(filename=filename) - - with open(filename, "a") as configfile: - configfile.writelines(save) - - -def net_restart(): - ''' - restarts LXC networking - ''' - cmd = ['/usr/sbin/service lxc-net restart'] - try: - subprocess.check_call(cmd, shell=True) - return 0 - except CalledProcessError: - return 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..b68be44 --- /dev/null +++ b/lwp/app/views.py @@ -0,0 +1,848 @@ +#!/usr/bin/env python +# encoding: utf-8 +from __future__ import absolute_import + +from functools import wraps +import lwp.lxclite as lxc +import lwp.lxc as lwp +import subprocess +import time +import re +import hashlib +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 + + +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'), 403 + + return wrap + + +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('/home') +@if_auth +def home(): + ''' + home page function + ''' + + listx = lxc.listx() + containers_all = [] + + for status in ['RUNNING', 'FROZEN', 'STOPPED']: + 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': settings + }) + 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()) + + +@app.route('/') +@if_auth +def index(): + return redirect(url_for('home'), code=302) + + +@app.route('/about') +@if_auth +def about(): + ''' + about page + ''' + + return render_template('about.html', containers=lxc.ls(), version=lwp.check_version()) + + +@app.route('//edit', methods=['POST', 'GET']) +@if_auth +def edit(container=None): + ''' + edit containers page and actions if form post request + ''' + + 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) + + +@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 + ''' + + 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'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()) + + +@app.route('/lwp/users', methods=['POST', 'GET']) +@if_superuser +@if_auth +def lwp_users(): + ''' + returns users and get posts request : can edit or add user in page. + this funtction uses sqlite3 + ''' + 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) + + +@app.route('/checkconfig') +@if_superuser +def checkconfig(): + ''' + returns the display of lxc-checkconfig command + ''' + 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 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')) + + +@app.route('/action/create-container', methods=['GET', 'POST']) +@if_auth +@if_superuser +def create_container(): + ''' + verify all forms to create a container + ''' + 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')) + + +@app.route('/action/clone-container', methods=['GET', 'POST']) +@if_auth +@if_superuser +def clone_container(): + ''' + verify all forms to clone a container + ''' + 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')) + + +@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.clear() + flash(u'You are logged out!', 'success') + return redirect(url_for('login')) + + +@app.route('/_refresh_cpu_host') +@if_auth +@ObjectCacher(timeout=5) +def refresh_cpu_host(): + return lwp.host_cpu_percent() + + +@app.route('/_refresh_uptime_host') +@if_auth +@ObjectCacher(timeout=5) +def refresh_uptime_host(): + return jsonify(lwp.host_uptime()) + + +@app.route('/_refresh_disk_host') +@if_auth +@ObjectCacher(timeout=5) +def refresh_disk_host(): + 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 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=3600) +def check_version(): + 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/LICENSE b/lwp/lxc/LICENSE similarity index 100% rename from lwp/LICENSE rename to lwp/lxc/LICENSE diff --git a/lwp/lxc/__init__.py b/lwp/lxc/__init__.py new file mode 100644 index 0000000..7ebe414 --- /dev/null +++ b/lwp/lxc/__init__.py @@ -0,0 +1,457 @@ +# 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 sys +from ..lxclite import exists, stopped, ContainerDoesntExists + +import os +import platform +import re +import subprocess +import time + +from io import StringIO + +try: + from urllib.request import urlopen +except ImportError: + from urllib2 import urlopen + +try: + import configparser +except ImportError: + import ConfigParser as configparser + + +class CalledProcessError(Exception): + pass + +cgroup = {} +cgroup['type'] = 'lxc.network.type' +cgroup['link'] = 'lxc.network.link' +cgroup['flags'] = 'lxc.network.flags' +cgroup['hwaddr'] = 'lxc.network.hwaddr' +cgroup['rootfs'] = 'lxc.rootfs' +cgroup['utsname'] = 'lxc.utsname' +cgroup['arch'] = 'lxc.arch' +cgroup['ipv4'] = 'lxc.network.ipv4' +cgroup['memlimit'] = 'lxc.cgroup.memory.limit_in_bytes' +cgroup['swlimit'] = 'lxc.cgroup.memory.memsw.limit_in_bytes' +cgroup['cpus'] = 'lxc.cgroup.cpuset.cpus' +cgroup['shares'] = 'lxc.cgroup.cpu.shares' +cgroup['deny'] = 'lxc.cgroup.devices.deny' +cgroup['allow'] = 'lxc.cgroup.devices.allow' + + +def FakeSection(fp): + content = u"[DEFAULT]\n%s" % fp.read() + + return StringIO(content) + + +def DelSection(filename=None): + if filename: + load = open(filename, 'r') + read = load.readlines() + load.close() + i = 0 + while i < len(read): + if '[DEFAULT]' in read[i]: + del read[i] + break + load = open(filename, 'w') + load.writelines(read) + load.close() + + +def file_exist(filename): + ''' + checks if a given file exist or not + ''' + try: + with open(filename) as f: + f.close() + return True + except IOError: + return False + + +def ls_auto(): + ''' + returns a list of autostart containers + ''' + try: + auto_list = os.listdir('/etc/lxc/auto/') + except OSError: + auto_list = [] + return auto_list + + +def memory_usage(name): + ''' + returns memory usage in MB + ''' + if not exists(name): + raise ContainerDoesntExists( + "The container (%s) does not exist!" % name) + + if name in stopped(): + return 0 + + cmd = ['lxc-cgroup -n %s memory.usage_in_bytes' % name] + try: + out = subprocess.check_output(cmd, shell=True, + universal_newlines=True).splitlines() + except: + return 0 + return int(out[0]) / 1024 / 1024 + + +def host_memory_usage(): + ''' + returns a dict of host memory usage values + {'percent': int((used/total)*100), + 'percent_cached':int((cached/total)*100), + 'used': int(used/1024), + 'total': int(total/1024)} + ''' + out = open('/proc/meminfo') + for line in out: + if 'MemTotal:' == line.split()[0]: + split = line.split() + total = float(split[1]) + if 'MemFree:' == line.split()[0]: + split = line.split() + free = float(split[1]) + if 'Buffers:' == line.split()[0]: + split = line.split() + buffers = float(split[1]) + if 'Cached:' == line.split()[0]: + split = line.split() + 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)} + + +def host_cpu_percent(): + ''' + returns CPU usage in percent + ''' + f = open('/proc/stat', 'r') + line = f.readlines()[0] + data = line.split() + previdle = float(data[4]) + prevtotal = float(data[1]) + float(data[2]) + \ + float(data[3]) + float(data[4]) + f.close() + time.sleep(0.1) + f = open('/proc/stat', 'r') + line = f.readlines()[0] + data = line.split() + idle = float(data[4]) + total = float(data[1]) + float(data[2]) + float(data[3]) + float(data[4]) + f.close() + intervaltotal = total - prevtotal + percent = 100 * (intervaltotal - (idle - previdle)) / intervaltotal + return str('%.1f' % percent) + + +def host_disk_usage(directory='/var/lib/lxc'): + ''' + returns a dict of disk usage values + {'total': usage[1], + 'used': usage[2], + 'free': usage[3], + 'percent': usage[4]} + ''' + usage = subprocess.check_output(['df -h "%s"' % directory], + universal_newlines=True, + shell=True).split('\n')[1].split() + return {'total': usage[1], + 'used': usage[2], + 'free': usage[3], + 'percent': usage[4]} + + +def host_uptime(): + ''' + returns a dict of the system uptime + {'day': days, + 'time': '%d:%02d' % (hours,minutes)} + ''' + f = open('/proc/uptime') + uptime = int(f.readlines()[0].split('.')[0]) + minutes = uptime / 60 % 60 + hours = uptime / 60 / 60 % 24 + days = uptime / 60 / 60 / 24 + f.close() + return {'day': days, + 'time': '%d:%02d' % (hours, minutes)} + + +def check_ubuntu(): + ''' + return the System version + ''' + dist = '%s %s' % (platform.linux_distribution()[0], + platform.linux_distribution()[1]) + return dist + + +def get_templates_list(): + ''' + returns a sorted lxc templates list + ''' + templates = [] + path = None + + try: + path = os.listdir('/usr/share/lxc/templates') + except: + path = os.listdir('/usr/lib/lxc/templates') + + if path: + for line in path: + templates.append(line.replace('lxc-', '')) + + return sorted(templates) + + +def check_version(): + ''' + returns latest LWP version (dict with current and latest) + ''' + return {'current': None } + + +def get_net_settings(): + ''' + returns a dict of all known settings for LXC networking + ''' + filename = '/etc/default/lxc-net' + if not file_exist(filename): + filename = '/etc/default/lxc' + if not file_exist(filename): + return False + config = configparser.SafeConfigParser() + cfg = {} + config.readfp(FakeSection(open(filename))) + 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 + + +def get_container_settings(name): + ''' + returns a dict of all utils settings for a container + ''' + + if os.geteuid(): + filename = os.path.expanduser('~/.local/share/lxc/%s/config' % name) + else: + filename = '/var/lib/lxc/%s/config' % name + + if not file_exist(filename): + return False + config = configparser.SafeConfigParser() + cfg = {} + config.readfp(FakeSection(open(filename))) + try: + cfg['type'] = config.get('DEFAULT', cgroup['type']) + except configparser.NoOptionError: + cfg['type'] = '' + try: + cfg['link'] = config.get('DEFAULT', cgroup['link']) + except configparser.NoOptionError: + cfg['link'] = '' + try: + cfg['flags'] = config.get('DEFAULT', cgroup['flags']) + except configparser.NoOptionError: + cfg['flags'] = '' + try: + cfg['hwaddr'] = config.get('DEFAULT', cgroup['hwaddr']) + except configparser.NoOptionError: + cfg['hwaddr'] = '' + try: + cfg['rootfs'] = config.get('DEFAULT', cgroup['rootfs']) + except configparser.NoOptionError: + cfg['rootfs'] = '' + try: + cfg['utsname'] = config.get('DEFAULT', cgroup['utsname']) + except configparser.NoOptionError: + cfg['utsname'] = '' + try: + cfg['arch'] = config.get('DEFAULT', cgroup['arch']) + except configparser.NoOptionError: + cfg['arch'] = '' + try: + cfg['ipv4'] = config.get('DEFAULT', cgroup['ipv4']) + except configparser.NoOptionError: + cfg['ipv4'] = '' + try: + cfg['memlimit'] = re.sub(r'[a-zA-Z]', '', + config.get('DEFAULT', cgroup['memlimit'])) + except configparser.NoOptionError: + cfg['memlimit'] = '' + try: + cfg['swlimit'] = re.sub(r'[a-zA-Z]', '', + config.get('DEFAULT', cgroup['swlimit'])) + except configparser.NoOptionError: + cfg['swlimit'] = '' + try: + cfg['cpus'] = config.get('DEFAULT', cgroup['cpus']) + except configparser.NoOptionError: + cfg['cpus'] = '' + try: + cfg['shares'] = config.get('DEFAULT', cgroup['shares']) + except configparser.NoOptionError: + cfg['shares'] = '' + + if '%s.conf' % name in ls_auto(): + cfg['auto'] = True + else: + cfg['auto'] = False + + return cfg + + +def push_net_value(key, value, filename='/etc/default/lxc'): + ''' + replace a var in the lxc-net config file + ''' + if filename: + config = configparser.RawConfigParser() + config.readfp(FakeSection(open(filename))) + if not value: + config.remove_option('DEFAULT', key) + else: + config.set('DEFAULT', key, value) + + with open(filename, 'wb') as configfile: + config.write(configfile) + + DelSection(filename=filename) + + load = open(filename, 'r') + read = load.readlines() + load.close() + i = 0 + while i < len(read): + if ' = ' in read[i]: + split = read[i].split(' = ') + split[1] = split[1].strip('\n') + if '\"' in split[1]: + read[i] = '%s=%s\n' % (split[0].upper(), split[1]) + else: + read[i] = '%s=\"%s\"\n' % (split[0].upper(), split[1]) + i += 1 + load = open(filename, 'w') + load.writelines(read) + load.close() + + +def push_config_value(key, value, container=None): + ''' + replace a var in a container config file + ''' + + def save_cgroup_devices(filename=None): + ''' + returns multiple values (lxc.cgroup.devices.deny and + lxc.cgroup.devices.allow) in a list because configparser cannot + make this... + ''' + if filename: + values = [] + i = 0 + + load = open(filename, 'r') + read = load.readlines() + load.close() + + while i < len(read): + if not read[i].startswith('#') and \ + re.match('lxc.cgroup.devices.deny|' + 'lxc.cgroup.devices.allow', read[i]): + values.append(read[i]) + i += 1 + return values + + if container: + if os.geteuid(): + filename = os.path.expanduser('~/.local/share/lxc/%s/config' % + container) + else: + filename = '/var/lib/lxc/%s/config' % container + + save = save_cgroup_devices(filename=filename) + + config = configparser.RawConfigParser() + config.readfp(FakeSection(open(filename))) + if not value: + config.remove_option('DEFAULT', key) + elif key == cgroup['memlimit'] or key == cgroup['swlimit'] \ + and value is not False: + config.set('DEFAULT', key, '%sM' % value) + else: + config.set('DEFAULT', key, value) + + # Bugfix (can't duplicate keys with config parser) + if config.has_option('DEFAULT', cgroup['deny']) or \ + config.has_option('DEFAULT', cgroup['allow']): + config.remove_option('DEFAULT', cgroup['deny']) + config.remove_option('DEFAULT', cgroup['allow']) + + with open(filename, 'wb') as configfile: + config.write(configfile) + + DelSection(filename=filename) + + with open(filename, "a") as configfile: + configfile.writelines(save) + + +def net_restart(): + ''' + restarts LXC networking + ''' + cmd = ['/usr/sbin/service lxc-net restart'] + try: + subprocess.check_call(cmd, shell=True) + return 0 + except CalledProcessError: + return 1 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 76% rename from lxclite/__init__.py rename to lwp/lxclite/__init__.py index 2dc6cc0..803c286 100644 --- a/lxclite/__init__.py +++ b/lwp/lxclite/__init__.py @@ -26,8 +26,14 @@ # 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 +import logging + +log = logging.getLogger("lxclite") + +# TODO: Use it everywhere. def _run(cmd, output=False): @@ -44,8 +50,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 0 + except subprocess.CalledProcessError as e: + return e.returncode class ContainerAlreadyExists(Exception): @@ -94,6 +103,12 @@ def create(container, template='ubuntu', storage=None, xargs=None): if xargs: command += ' -- {}'.format(xargs) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run(command) @@ -111,9 +126,16 @@ def clone(orig=None, new=None, snapshot=False): if snapshot: command += ' -s' + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run(command) +@ObjectCacher(oid="lxc.info", timeout=5) def info(container): ''' Check info from lxc-info @@ -123,20 +145,25 @@ def info(container): raise ContainerDoesntExists( 'Container {} does not exist!'.format(container)) - output = _run('lxc-info -qn {}|grep -i "State\|PID"'.format(container), - output=True).splitlines() + output = _run('lxc-info -qn {}|grep -i "State\|PID\|IP"'.format(container), + output=True) - 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(oid="lxc.list", timeout=20) def ls(): ''' List containers directory @@ -207,6 +234,12 @@ def start(container): raise ContainerAlreadyRunning( 'Container {} is already running!'.format(container)) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run('lxc-start -dn {}'.format(container)) @@ -223,6 +256,12 @@ def stop(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run('lxc-stop -n {}'.format(container)) @@ -239,6 +278,12 @@ def freeze(container): raise ContainerNotRunning( 'Container {} is not running!'.format(container)) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run('lxc-freeze -n {}'.format(container)) @@ -255,6 +300,12 @@ def unfreeze(container): raise ContainerNotRunning( 'Container {} is not frozen!'.format(container)) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run('lxc-unfreeze -n {}'.format(container)) @@ -267,9 +318,16 @@ def destroy(container): raise ContainerDoesntExists( 'Container {} does not exists!'.format(container)) + try: + ObjectCacher.invalidate("lxc.info") + ObjectCacher.invalidate("lxc.list") + except KeyError as e: + log.error(e) + return _run('lxc-destroy -n {}'.format(container)) +@ObjectCacher(oid="lxc.chkconfig", timeout=5) def checkconfig(): ''' Returns the output of lxc-checkconfig (colors cleared) @@ -290,4 +348,10 @@ def cgroup(container, key, value): raise ContainerDoesntExists( 'Container {} does not exist!'.format(container)) + 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)) 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 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..d7eea44 --- /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-pre4' +__author__ = 'Élie Deloumeau, Antoine Tanzilli' + + +supports = { + 'install_requires': [ + 'flask==0.9', + 'arconfig', + 'object_cacher', + ] +} + +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 +)