Compare commits
3 commits
be299901e9
...
7cae5e1f22
Author | SHA1 | Date | |
---|---|---|---|
Théophile Bastian | 7cae5e1f22 | ||
Théophile Bastian | 915875ecf2 | ||
Théophile Bastian | cae5e2244c |
69
README.md
69
README.md
|
@ -1,3 +1,70 @@
|
||||||
# lxc-network
|
# 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.
|
||||||
|
|
|
@ -6,7 +6,6 @@ from .jinja_template import JinjaTemplate
|
||||||
|
|
||||||
import libvirt
|
import libvirt
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import getpass
|
import getpass
|
||||||
import weakref
|
import weakref
|
||||||
|
@ -75,7 +74,8 @@ class OverlayDirectory:
|
||||||
self.mount_point,
|
self.mount_point,
|
||||||
]
|
]
|
||||||
|
|
||||||
subprocess.run(command)
|
util.run_cmd_retry(command)
|
||||||
|
|
||||||
self.mounted = True
|
self.mounted = True
|
||||||
|
|
||||||
self.temp_dir_cleaner = weakref.finalize(
|
self.temp_dir_cleaner = weakref.finalize(
|
||||||
|
@ -91,7 +91,7 @@ class OverlayDirectory:
|
||||||
"umount",
|
"umount",
|
||||||
self.mount_point,
|
self.mount_point,
|
||||||
]
|
]
|
||||||
subprocess.run(command)
|
util.run_cmd_retry(command)
|
||||||
self.mounted = False
|
self.mounted = False
|
||||||
|
|
||||||
def _set_perms_for_cleanup(self):
|
def _set_perms_for_cleanup(self):
|
||||||
|
@ -109,7 +109,8 @@ class OverlayDirectory:
|
||||||
getpass.getuser(),
|
getpass.getuser(),
|
||||||
self.temp_dir.resolve(),
|
self.temp_dir.resolve(),
|
||||||
]
|
]
|
||||||
subprocess.run(command_chown)
|
util.run_cmd_retry(command_chown)
|
||||||
|
|
||||||
command_chmod = [
|
command_chmod = [
|
||||||
"sudo",
|
"sudo",
|
||||||
"chmod",
|
"chmod",
|
||||||
|
@ -117,7 +118,7 @@ class OverlayDirectory:
|
||||||
"700",
|
"700",
|
||||||
self.temp_dir.resolve(),
|
self.temp_dir.resolve(),
|
||||||
]
|
]
|
||||||
subprocess.run(command_chmod)
|
util.run_cmd_retry(command_chmod)
|
||||||
|
|
||||||
def cleanup_mount(self):
|
def cleanup_mount(self):
|
||||||
self._umount()
|
self._umount()
|
||||||
|
|
76
lxc_net/parse_network.py
Normal file
76
lxc_net/parse_network.py
Normal 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
|
||||||
|
]
|
|
@ -3,6 +3,9 @@
|
||||||
from . import settings
|
from . import settings
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class NumberedClass:
|
class NumberedClass:
|
||||||
""" A class that counts its current instance number """
|
""" A class that counts its current instance number """
|
||||||
|
@ -125,3 +128,11 @@ class Addrv6:
|
||||||
return "{base_range}:{link_id:04x}::{dev_id:04x}".format(
|
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,
|
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
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
libvirt-python
|
libvirt-python
|
||||||
jinja2
|
jinja2
|
||||||
|
pyyaml
|
||||||
|
|
62
spawn_network.py
Executable file
62
spawn_network.py
Executable 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()
|
Loading…
Reference in a new issue