Compare commits

...

6 commits

Author SHA1 Message Date
Théophile Bastian 95080f88ad Implement containers 2020-03-09 17:36:57 +01:00
Théophile Bastian 603ba68f18 templates: load from templates/ directory 2020-03-09 17:36:38 +01:00
Théophile Bastian e310032b36 Network: do not check existence at __init__
This causes a verbose error on normal behaviour that cannot be muted
2020-03-09 17:09:35 +01:00
Théophile Bastian 5f3e504913 Network: factor out some parts for future Container 2020-03-09 13:00:43 +01:00
Théophile Bastian 25b0b714e1 Templates: use jinja2 2020-03-09 13:00:10 +01:00
Théophile Bastian 003e774fc4 Use virtualenv, rename src -> lxc_net
Fix imports accordingly
2020-03-09 12:58:57 +01:00
13 changed files with 300 additions and 59 deletions

2
.gitignore vendored
View file

@ -58,3 +58,5 @@ docs/_build/
# PyBuilder
target/
# Virtualenv
venv/

0
lxc_net/__init__.py Normal file
View file

186
lxc_net/container.py Normal file
View file

@ -0,0 +1,186 @@
""" Container objects """
from . import settings
from . import util
from .xml_template import XMLTemplate
import libvirt
import tempfile
import subprocess
from pathlib import Path
import getpass
import weakref
class OverlayDirectory:
""" Allocates an overlayfs with a given lower dir and a temporary upper dir. The
overlayfs is destroyed when this object is deallocated """
class DoesNotExist(Exception):
""" Raised if `lower` does not exist """
def __init__(self, path):
self.path = path
def __str__(self):
return "Path {} does not exist".format(self.path)
def __init__(self, lower, name=None):
if not name:
name = ""
self.lower = Path(lower)
if not self.lower.exists():
raise self.DoesNotExist(self.lower)
root_temp_dir = Path(settings.OVERLAYFS_BASE_DIR)
if not root_temp_dir.exists():
root_temp_dir.mkdir()
self.overlay_temp_dir = tempfile.TemporaryDirectory(
prefix=name + ("_" if name else ""), dir=root_temp_dir.resolve(),
)
self.temp_dir = Path(self.overlay_temp_dir.name)
self.temp_dir_cleaner = None
self.upper = self.temp_dir / "upper"
self.work = self.temp_dir / "work"
self.mount_point = self.temp_dir / "mount"
self.upper.mkdir()
self.work.mkdir()
self.mount_point.mkdir()
self.mounted = False
self._mount()
def _mount(self):
if self.mounted:
return
command = [
"sudo",
"mount",
"-t",
"overlay",
"overlay",
"-o",
"lowerdir={lower},upperdir={upper},workdir={work}".format(
lower=self.lower.resolve(),
upper=self.upper.resolve(),
work=self.work.resolve(),
),
self.mount_point,
]
subprocess.run(command)
self.mounted = True
self.temp_dir_cleaner = weakref.finalize(
self.overlay_temp_dir, self.cleanup_mount
)
def _umount(self):
if not self.mounted:
return
command = [
"sudo",
"umount",
self.mount_point,
]
subprocess.run(command)
self.mounted = False
def _set_perms_for_cleanup(self):
""" Sets the permissions to allow cleanup """
if not self.temp_dir.exists():
return # Already deleted
# The overlayfs runs as root, so we need to allow our current user to delete
# the temp directory files
command_chown = [
"sudo",
"chown",
"-R",
getpass.getuser(),
self.temp_dir.resolve(),
]
subprocess.run(command_chown)
command_chmod = [
"sudo",
"chmod",
"-R",
"700",
self.temp_dir.resolve(),
]
subprocess.run(command_chmod)
def cleanup_mount(self):
self._umount()
self._set_perms_for_cleanup()
class Container(util.LibvirtObject):
class AlreadyExists(Exception):
def __str__(self):
return "This container is already instanciated"
def __init__(self, conn, networks, name=None, mem=int(1e6)):
super().__init__(conn)
if not name:
name = str(self.id)
self.name = settings.PREFIX + "_dom_" + name
self.networks = networks
self.mem = mem
self.overlay_root = None
self.lxc_container = None
def get_jinja_networks(self):
""" Get a jinja2 environment-compatible network list """
return [
{"mac": util.MACAddress(net.id, self.id), "name": net.name, "net": net}
for net in self.networks
]
def create(self):
if self.lxc_container:
raise self.AlreadyExists()
self.overlay_root = OverlayDirectory(settings.BASE_SYSTEM_ROOT, name=self.name)
xml = XMLTemplate("container.xml").inst(
name=self.name,
uuid=self.uuid,
mem=self.mem,
filesystem_root=self.overlay_root.mount_point,
networks=self.get_jinja_networks(),
)
self.lxc_container = self.conn.createXML(xml)
def cleanup(self):
if self.lxc_container:
try:
self.lxc_container.destroy()
except libvirt.libvirtError as exn:
if not str(exn).startswith("Domain not found:"):
raise exn
# Else, the machine was already stopped: everything is fine
def __enter__(self):
self.create()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.cleanup()
def __del__(self):
self.cleanup()

View file

@ -1,56 +1,33 @@
""" Network objects """
import settings
import util
from xml_template import XMLTemplate
import uuid
import libvirt
from . import settings
from . import util
from .xml_template import XMLTemplate
class Network(util.NumberedClass):
class Network(util.LibvirtObject):
class AlreadyExists(Exception):
def __str__(self):
return "This network is already instanciated"
class TooMany(Exception):
def __init__(self, count):
self.count = count
def __str__(self):
return (
"Limit number reached. The current instance #{} does not fit in IPv4"
).format(self.count)
def __init__(self, conn, name=None):
super().__init__()
if self.id > 250:
raise self.TooMany(self.id)
super().__init__(conn)
if not name:
name = str(self.id)
self.conn = conn
self.uuid = uuid.uuid4()
self.name = settings.PREFIX + "_" + name
self.name = settings.PREFIX + "_link_" + name
self.bridge_id = settings.NETWORK_ID * 0xFF + self.id
self.bridge_mac = util.MACAddress(self.id, None)
self.ipv4 = util.Addrv4(self.id, None)
self.ipv6 = util.Addrv6(self.id, None)
self.lxc_network = None
try:
if self.conn.networkLookupByName(self.name):
raise self.AlreadyExists()
except libvirt.libvirtError:
pass
def create(self):
if self.lxc_network:
raise self.AlreadyExists()
xml = XMLTemplate("templates/network.xml").inst(
xml = XMLTemplate("network.xml").inst(
name=self.name,
uuid=self.uuid,
bridge_id=self.bridge_id,

View file

@ -7,7 +7,10 @@ PREFIX = "testnw"
CONTAINER_BASE_ROOT = "/var/lib/machines/lxc-base-" + PREFIX
# Overlayfs mount dir
OVERLAYFS_MOUNT_DIR = "/tmp/{}-overlays/".format(PREFIX)
OVERLAYFS_BASE_DIR = "/tmp/{}-overlays/".format(PREFIX)
# Base root
BASE_SYSTEM_ROOT = "/home/tobast/Machines/lxc-network/_base"
# The ID of the whole generated network -- below 0xff
NETWORK_ID = 132

View file

@ -0,0 +1,36 @@
<domain type="lxc">
<name>{{ name }}</name>
<uuid>{{ uuid }}</uuid>
<memory unit="KiB">{{ mem }}</memory>
<vcpu placement="static">1</vcpu>
<os>
<type arch="x86_64">exe</type>
<init>/sbin/init</init>
</os>
<features>
<privnet/>
</features>
<clock offset="utc"/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>destroy</on_crash>
<devices>
<emulator>/usr/lib/libvirt/libvirt_lxc</emulator>
<filesystem type="mount" accessmode="mapped">
<source dir="{{ filesystem_root }}"/>
<target dir="/"/>
</filesystem>
{% for net in networks %}
<interface type="network">
<mac address="{{ net.mac }}"/>
<source network="{{ net.name }}"/>
</interface>
{% endfor %}
<console type="pty">
<target type="lxc" port="0"/>
</console>
</devices>
</domain>

View file

@ -0,0 +1,11 @@
<network>
<name>{{ name }}</name>
<uuid>{{ uuid }}</uuid>
<bridge name='virbr{{ bridge_id }}' stp='on' delay='0'/>
<mac address='{{ mac }}'/>
<domain name='{{ name }}'/>
<ip address='{{ addrv4 }}' netmask='{{ netmaskv4 }}'>
</ip>
<ip family='ipv6' address='{{ addrv6 }}' prefix='{{ prefixv6 }}'>
</ip>
</network>

View file

@ -1,6 +1,7 @@
""" Various utils """
import settings
from . import settings
import uuid
class NumberedClass:
@ -18,6 +19,33 @@ class NumberedClass:
self.id = self.get_id()
class LibvirtObject(NumberedClass):
""" A class that has the basic attributes of a libvirt objects:
* id
* uuid
and the applicable restrictions
"""
class TooMany(Exception):
def __init__(self, count):
self.count = count
def __str__(self):
return (
"Limit number reached. The current instance #{} does not fit in IPv4"
).format(self.count)
def __init__(self, conn):
super().__init__()
self.conn = conn
if self.id > 250:
raise self.TooMany(self.id)
self.uuid = uuid.uuid4()
class MACAddress:
""" A MAC address for a NIC or bridge """

23
lxc_net/xml_template.py Normal file
View file

@ -0,0 +1,23 @@
""" Reads an XML template from a file """
import os
import jinja2
class XMLTemplate:
""" Reads and instanciates a template from a file """
base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
env = jinja2.Environment(
loader=jinja2.FileSystemLoader(base_path),
autoescape=jinja2.select_autoescape(
enabled_extensions=("xml"), default_for_string=True,
),
)
def __init__(self, path):
self.template = self.env.get_template(path)
def inst(self, *args, **kwargs):
""" instanciates the template """
return self.template.render(*args, **kwargs)

2
requirements.txt Normal file
View file

@ -0,0 +1,2 @@
libvirt-python
jinja2

View file

@ -1,11 +0,0 @@
<network>
<name>{name}</name>
<uuid>{uuid}</uuid>
<bridge name='virbr{bridge_id}' stp='on' delay='0'/>
<mac address='{mac}'/>
<domain name='{name}'/>
<ip address='{addrv4}' netmask='{netmaskv4}'>
</ip>
<ip family='ipv6' address='{addrv6}' prefix='{prefixv6}'>
</ip>
</network>

View file

@ -1,16 +0,0 @@
""" Reads an XML template from a file """
import os
class XMLTemplate:
""" Reads and instanciates a template from a file """
def __init__(self, path):
self.path = os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
with open(self.path, "r") as handle:
self.template_str = handle.read()
def inst(self, *args, **kwargs):
""" instanciates the template """
return self.template_str.format(*args, **kwargs)