Compare commits
6 commits
260417bcd0
...
95080f88ad
Author | SHA1 | Date | |
---|---|---|---|
Théophile Bastian | 95080f88ad | ||
Théophile Bastian | 603ba68f18 | ||
Théophile Bastian | e310032b36 | ||
Théophile Bastian | 5f3e504913 | ||
Théophile Bastian | 25b0b714e1 | ||
Théophile Bastian | 003e774fc4 |
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -58,3 +58,5 @@ docs/_build/
|
||||||
# PyBuilder
|
# PyBuilder
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Virtualenv
|
||||||
|
venv/
|
||||||
|
|
0
lxc_net/__init__.py
Normal file
0
lxc_net/__init__.py
Normal file
186
lxc_net/container.py
Normal file
186
lxc_net/container.py
Normal 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()
|
|
@ -1,56 +1,33 @@
|
||||||
""" Network objects """
|
""" Network objects """
|
||||||
|
|
||||||
import settings
|
from . import settings
|
||||||
import util
|
from . import util
|
||||||
from xml_template import XMLTemplate
|
from .xml_template import XMLTemplate
|
||||||
|
|
||||||
import uuid
|
|
||||||
import libvirt
|
|
||||||
|
|
||||||
|
|
||||||
class Network(util.NumberedClass):
|
class Network(util.LibvirtObject):
|
||||||
class AlreadyExists(Exception):
|
class AlreadyExists(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "This network is already instanciated"
|
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):
|
def __init__(self, conn, name=None):
|
||||||
super().__init__()
|
super().__init__(conn)
|
||||||
|
|
||||||
if self.id > 250:
|
|
||||||
raise self.TooMany(self.id)
|
|
||||||
|
|
||||||
if not name:
|
if not name:
|
||||||
name = str(self.id)
|
name = str(self.id)
|
||||||
|
|
||||||
self.conn = conn
|
self.name = settings.PREFIX + "_link_" + name
|
||||||
self.uuid = uuid.uuid4()
|
|
||||||
self.name = settings.PREFIX + "_" + name
|
|
||||||
self.bridge_id = settings.NETWORK_ID * 0xFF + self.id
|
self.bridge_id = settings.NETWORK_ID * 0xFF + self.id
|
||||||
self.bridge_mac = util.MACAddress(self.id, None)
|
self.bridge_mac = util.MACAddress(self.id, None)
|
||||||
self.ipv4 = util.Addrv4(self.id, None)
|
self.ipv4 = util.Addrv4(self.id, None)
|
||||||
self.ipv6 = util.Addrv6(self.id, None)
|
self.ipv6 = util.Addrv6(self.id, None)
|
||||||
self.lxc_network = None
|
self.lxc_network = None
|
||||||
|
|
||||||
try:
|
|
||||||
if self.conn.networkLookupByName(self.name):
|
|
||||||
raise self.AlreadyExists()
|
|
||||||
except libvirt.libvirtError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
if self.lxc_network:
|
if self.lxc_network:
|
||||||
raise self.AlreadyExists()
|
raise self.AlreadyExists()
|
||||||
|
|
||||||
xml = XMLTemplate("templates/network.xml").inst(
|
xml = XMLTemplate("network.xml").inst(
|
||||||
name=self.name,
|
name=self.name,
|
||||||
uuid=self.uuid,
|
uuid=self.uuid,
|
||||||
bridge_id=self.bridge_id,
|
bridge_id=self.bridge_id,
|
|
@ -7,7 +7,10 @@ PREFIX = "testnw"
|
||||||
CONTAINER_BASE_ROOT = "/var/lib/machines/lxc-base-" + PREFIX
|
CONTAINER_BASE_ROOT = "/var/lib/machines/lxc-base-" + PREFIX
|
||||||
|
|
||||||
# Overlayfs mount dir
|
# 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
|
# The ID of the whole generated network -- below 0xff
|
||||||
NETWORK_ID = 132
|
NETWORK_ID = 132
|
36
lxc_net/templates/container.xml
Normal file
36
lxc_net/templates/container.xml
Normal 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>
|
||||||
|
|
11
lxc_net/templates/network.xml
Normal file
11
lxc_net/templates/network.xml
Normal 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>
|
|
@ -1,6 +1,7 @@
|
||||||
""" Various utils """
|
""" Various utils """
|
||||||
|
|
||||||
import settings
|
from . import settings
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class NumberedClass:
|
class NumberedClass:
|
||||||
|
@ -18,6 +19,33 @@ class NumberedClass:
|
||||||
self.id = self.get_id()
|
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:
|
class MACAddress:
|
||||||
""" A MAC address for a NIC or bridge """
|
""" A MAC address for a NIC or bridge """
|
||||||
|
|
23
lxc_net/xml_template.py
Normal file
23
lxc_net/xml_template.py
Normal 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
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
libvirt-python
|
||||||
|
jinja2
|
|
@ -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>
|
|
|
@ -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)
|
|
Loading…
Reference in a new issue