diff --git a/lxc_net/parse_network.py b/lxc_net/parse_network.py new file mode 100644 index 0000000..674299d --- /dev/null +++ b/lxc_net/parse_network.py @@ -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 + ] diff --git a/requirements.txt b/requirements.txt index d298a2e..60ffc28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ libvirt-python jinja2 +pyyaml diff --git a/spawn_network.py b/spawn_network.py new file mode 100755 index 0000000..aad7ec2 --- /dev/null +++ b/spawn_network.py @@ -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()