Compare commits

...

3 commits

6 changed files with 224 additions and 6 deletions

View file

@ -1,3 +1,70 @@
# lxc-network
A network of LXC containers, managed through libvirt
A network of LXC containers, managed through libvirt
## Dependencies
This script will most probably break on any other system than Linux, and will
definitely break on anything non-UNIX.
It relies on `libvirt` and `overlayfs`.
## Setup
It is recommended to set up `lxc-network` within a *virtualenv*:
```bash
virtualenv -p python3 venv
source venv/bin/activate
pip install -r requirements.txt
```
Furthermore, you are expected to set up a system root tree within the directory
of your choice, and put its path in `lxc_net/settings.py`. This can be done eg.
in ArchLinux with
```bash
pacstrap [your_root_directory] base
```
or the equivalent `debootstrap` command on Debian.
This system is expected to use `systemd`, and to have enabled
`systemd-networkd` to setup its IP addresses.
## Usage
You can spawn a network using
```bash
./spawn_network.py topology_description.yml
```
where `topology_description.yml` is a valid topology description file (see
below).
## Topology description file
A topology is described in a [YAML](https://en.wikipedia.org/wiki/YAML) file
looking like this:
```yaml
---
links:
- domains: ['a', 'b']
- domains: ['b', 'c']
domains:
b:
enable_v4: false
```
The `links` element is mandatory, each link containing a mandatory `domains`
attribute, the list of domains (containers) connected to it. A domain is
described by an arbitrary name. Domains will be spawned (and indexed) in
alphabetical order.
A `domains` root element is optional, and may be used to specify
domain-specific options.
The valid options are:
* `enable_v4`: boolean, specifies whether the domain has an IPv4 address.

View file

@ -6,7 +6,6 @@ from .jinja_template import JinjaTemplate
import libvirt
import tempfile
import subprocess
from pathlib import Path
import getpass
import weakref
@ -75,7 +74,8 @@ class OverlayDirectory:
self.mount_point,
]
subprocess.run(command)
util.run_cmd_retry(command)
self.mounted = True
self.temp_dir_cleaner = weakref.finalize(
@ -91,7 +91,7 @@ class OverlayDirectory:
"umount",
self.mount_point,
]
subprocess.run(command)
util.run_cmd_retry(command)
self.mounted = False
def _set_perms_for_cleanup(self):
@ -109,7 +109,8 @@ class OverlayDirectory:
getpass.getuser(),
self.temp_dir.resolve(),
]
subprocess.run(command_chown)
util.run_cmd_retry(command_chown)
command_chmod = [
"sudo",
"chmod",
@ -117,7 +118,7 @@ class OverlayDirectory:
"700",
self.temp_dir.resolve(),
]
subprocess.run(command_chmod)
util.run_cmd_retry(command_chmod)
def cleanup_mount(self):
self._umount()

76
lxc_net/parse_network.py Normal file
View file

@ -0,0 +1,76 @@
import yaml
import sys
from . import network, container
class YamlTopology:
""" Parse a YAML description of a network topology. The networks' links and domains
are contained in the `links` and `domains` attributes, but their `create` methods
are not called. """
class InvalidConfiguration(Exception):
def __init__(self, reason):
self.reason = reason
def __str__(self):
return "Bad topology configuration: {}".format(self.reason)
def __init__(self, path, conn):
self.path = path
self.conn = conn
self.domains = None
self.links = None
self._parse()
def _parse(self):
with open(self.path, "r") as handle:
topology = yaml.safe_load(handle)
if "links" not in topology:
raise self.InvalidConfiguration("links definition is mandatory")
link_descr = topology["links"]
self.links = []
dom_descr = {}
for link_conf in link_descr:
if "domains" not in link_conf:
raise self.InvalidConfiguration(
"a 'domains' attribute is mandatory for each link"
)
cur_link = network.Network(self.conn)
self.links.append(cur_link)
for dom in link_conf["domains"]:
if dom not in dom_descr:
dom_descr[dom] = {"links": []}
dom_descr[dom]["links"].append(cur_link)
for dom_conf_name in topology.get("domains", None):
if dom_conf_name not in dom_descr:
# Domain does not participate in any link: warn and ignore
print(
(
"WARNING: domain {} does not participate in any link. "
"Ignored."
).format(dom_conf_name),
file=sys.stderr,
)
continue
dom_conf = topology["domains"][dom_conf_name]
dom_descr[dom_conf_name]["enable_v4"] = dom_conf.get("enable_v4", True)
sorted_dom_names = sorted(list(dom_descr.keys()))
self.domains = [
container.Container(
self.conn,
dom_descr[dom]["links"],
enable_v4=dom_descr[dom].get("enable_v4", True),
)
for dom in sorted_dom_names
]

View file

@ -3,6 +3,9 @@
from . import settings
import uuid
import subprocess
import sys
class NumberedClass:
""" A class that counts its current instance number """
@ -125,3 +128,11 @@ class Addrv6:
return "{base_range}:{link_id:04x}::{dev_id:04x}".format(
base_range=settings.IPV6_RANGE, link_id=self.link_id, dev_id=self.dev_id,
)
def run_cmd_retry(command, *args, **kwargs):
rc = subprocess.run(command, *args, **kwargs)
while rc.returncode != 0:
print("Command failed. Try again:", file=sys.stderr)
rc = subprocess.run(command, *args, **kwargs)
return rc

View file

@ -1,2 +1,3 @@
libvirt-python
jinja2
pyyaml

62
spawn_network.py Executable file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python3
from lxc_net import parse_network
import sys
import signal
import libvirt
import argparse
def parse_args():
parser = argparse.ArgumentParser(description="Spawns a network of LXC containers.")
parser.add_argument("topology", help="A YAML file defining the network topology")
args = parser.parse_args()
return args
def main():
args = parse_args()
received_sigint = False
def handle_sigint(signum, frame):
""" Called upon SIGINT (^C) """
nonlocal received_sigint
print(" >> Received SIGINT, stopping network...")
received_sigint = True
signal.signal(signal.SIGINT, handle_sigint)
conn = libvirt.open("lxc:///")
topology = parse_network.YamlTopology(args.topology, conn)
print(">> Spawning networks: ", end="")
sys.stdout.flush()
for link in topology.links:
if received_sigint:
return
print(link.name, end="... ")
sys.stdout.flush()
link.create()
print("Done.")
print(">> Spawning containers: ", end="")
sys.stdout.flush()
for c_dom in topology.domains:
if received_sigint:
return
print(c_dom.name, end="... ")
sys.stdout.flush()
c_dom.create()
print("Done.")
print("Network running. Press ^C to terminate.")
while not received_sigint: # Wait for SIGINT
signal.pause()
if __name__ == "__main__":
main()