Skip to content

LXC

LXC GPLv3 license

apt update
apt install lxc lxc-templates bridge-utils
/etc/netplan/01-custom-netplan.yaml
network:
  version: 2
  renderer: networkd
  ethernets:
    eno1:
      dhcp4: no
  bridges:
    br0:
      interfaces: [eno1]
      dhcp4: no
      addresses: [51.91.223.76/24]
      gateway4: 51.91.223.254
      nameservers:
        addresses: [8.8.8.8, 8.8.4.4]
netplan apply

Create
lxc-create -t download -n testcontainer -- -d ubuntu -r focal -a amd64
/var/lib/lxc/testcontainer/config
lxc.net.0.type = veth
lxc.net.0.link = br0
lxc.net.0.flags = up
lxc.net.0.ipv4.address = 87.98.186.23/32
lxc.net.0.ipv4.gateway = 51.91.223.254
lxc.net.0.hwaddr = 02:00:00:0f:67:21
Start
tee /var/lib/lxc/testcontainer/rootfs/etc/profile.d/02-route.sh > /dev/null <<EOF
#!/bin/bash
ip addr add 87.98.186.23/32 dev eth0
ip route add 51.91.223.254 dev eth0
ip route add default via 51.91.223.254
EOF

lxc-start -n testcontainer
lxc-attach -n testcontainer -- /etc/profile.d/02-route.sh
lxc-attach -n testcontainer
Verify
ip a
ping 8.8.8.8
Destroy
lxc-stop -n testcontainer
lxc-destroy -n testcontainer
lxc-ls --fancy
rm -rf /var/lib/lxc/testcontainer/

Install
wget https://lxc-webpanel.github.io/tools/install.sh -O - | bash
apt install python3-pip
pip install flask
pip install distro
Run
python3 /srv/lwp/lwp/lwp.py
/srv/lwp/lwp/__init__.py
# 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 distro
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' % (distro.linux_distribution()[0],
                      distro.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_fname():
    filename = '/etc/default/lxc-net'
    if not file_exist(filename):
        filename = '/etc/default/lxc'
    if not file_exist(filename):
        filename = None
    return filename


def get_net_settings():
    '''
    returns a dict of all known settings for LXC networking
    '''
    filename = get_net_settings_fname()
    if not 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):
    '''
    replace a var in the lxc-net config file
    '''
    filename = get_net_settings_fname()

    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