diff --git a/lxc_net/container.py b/lxc_net/container.py index 408045d..659e3ba 100644 --- a/lxc_net/container.py +++ b/lxc_net/container.py @@ -154,6 +154,10 @@ class Container(util.LibvirtObject): self.lxc_container = None + @property + def up(self): + return self.lxc_container is not None + def get_jinja_networks(self): """ Get a jinja2 environment-compatible network list """ return [ diff --git a/lxc_net/network.py b/lxc_net/network.py index 2ea1612..c1ec5e7 100644 --- a/lxc_net/network.py +++ b/lxc_net/network.py @@ -25,6 +25,10 @@ class Network(util.LibvirtObject): self.ipv6 = util.Addrv6(self.id, None, host_address=True) self.lxc_network = None + @property + def up(self): + return self.lxc_network is not None + def create(self): if self.lxc_network: raise self.AlreadyExists() @@ -50,6 +54,7 @@ class Network(util.LibvirtObject): def cleanup(self): if self.lxc_network: self.lxc_network.destroy() + self.lxc_network = None def __enter__(self): self.create() diff --git a/lxc_net/parse_network.py b/lxc_net/parse_network.py index 9dc3d5f..e4836a6 100644 --- a/lxc_net/parse_network.py +++ b/lxc_net/parse_network.py @@ -1,12 +1,41 @@ import yaml import sys +from functools import wraps 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. """ +def requires_parsed(func): + @wraps(func) + def aux(self, *args, **kwargs): + if not self.parsed: + raise self.__class__.NotParsed + return func(self, *args, **kwargs) + + return aux + + +def requires_id_in_dom_range(func): + @wraps(func) + def aux(self, dom_id, *args, **kwargs): + if dom_id not in range(len(self.domains)): + raise IndexError("bad domain id: {}".format(dom_id)) + return func(self, dom_id, *args, **kwargs) + + return aux + + +def requires_id_in_link_range(func): + @wraps(func) + def aux(self, link_id, *args, **kwargs): + if link_id not in range(len(self.links)): + raise IndexError("bad domain id: {}".format(link_id)) + return func(self, link_id, *args, **kwargs) + + return aux + + +class Topology: + """ A generic network topology """ class InvalidConfiguration(Exception): def __init__(self, reason): @@ -15,13 +44,153 @@ class YamlTopology: def __str__(self): return "Bad topology configuration: {}".format(self.reason) - def __init__(self, path, conn): - self.path = path + class NotParsed(Exception): + def __str__(self): + return "This topology is not parsed yet" + + def __init__(self, conn): self.conn = conn + self.parsed = False self.domains = None self.links = None + @requires_parsed + @requires_id_in_link_range + def net_setstate_single(self, link_id, state, verbose=False, verbose_inline=False): + """ Start or stop a single network. + * `net_id`: the link to be affected + * `verbose`: if true, prints progress + """ + + link = self.links[link_id] + print_endchar = "\t" if verbose_inline else "\n" + + if link.up == state: + if verbose: + print( + "{} already {}.".format(link.name, "up" if state else "down"), + end=print_endchar, + ) + sys.stdout.flush() + return + + if verbose: + print("{}... ".format(link.name), end="") + sys.stdout.flush() + + if state is True: + link.create() + else: + link.cleanup() + + if verbose: + print("done.", end=print_endchar) + sys.stdout.flush() + + @requires_parsed + @requires_id_in_dom_range + def dom_setstate_single(self, dom_id, state, verbose=False, verbose_inline=False): + """ Start or stop a single domain. + * `dom_id`: the domain to be affected + * `verbose`: if true, prints progress + """ + + dom = self.domains[dom_id] + print_endchar = "\t" if verbose_inline else "\n" + + if dom.up == state: + if verbose: + print( + "{} already {}.".format(dom.name, "up" if state else "down"), + end=print_endchar, + ) + sys.stdout.flush() + return + + if verbose: + print("{}... ".format(dom.name), end="") + sys.stdout.flush() + + if state is True: + dom.create() + else: + dom.cleanup() + + if verbose: + print("done.", end=print_endchar) + sys.stdout.flush() + + def net_start_single(self, link_id, verbose=False, verbose_inline=False): + self.net_setstate_single(link_id, True, verbose, verbose_inline) + + def net_stop_single(self, link_id, verbose=False, verbose_inline=False): + self.net_setstate_single(link_id, False, verbose, verbose_inline) + + def dom_start_single(self, dom_id, verbose=False, verbose_inline=False): + self.dom_setstate_single(dom_id, True, verbose, verbose_inline) + + def dom_stop_single(self, dom_id, verbose=False, verbose_inline=False): + self.dom_setstate_single(dom_id, False, verbose, verbose_inline) + + @requires_parsed + def net_setstate(self, state, verbose=False): + """ Start or stop the networks. + * `verbose`: if true, prints progress + """ + + if verbose: + print("{} networks: ".format("Starting" if state else "Stopping"), end="\t") + sys.stdout.flush() + + for link_id in range(len(self.links)): + self.net_setstate_single(link_id, state, verbose, verbose_inline=True) + + if verbose: + print("") # New line + + @requires_parsed + def dom_setstate(self, state, verbose=False): + """ Start or stop the domains. + * `verbose`: if true, prints progress + """ + + if verbose: + print("{} domains: ".format("Starting" if state else "Stopping"), end="\t") + sys.stdout.flush() + + for dom_id in range(len(self.domains)): + self.dom_setstate_single(dom_id, state, verbose, verbose_inline=True) + + if verbose: + print("") # New line + + def net_start(self, verbose=False): + self.net_setstate(True, verbose) + + def net_stop(self, verbose=False): + self.net_setstate(False, verbose) + + def dom_start(self, verbose=False): + self.dom_setstate(True, verbose) + + def dom_stop(self, verbose=False): + self.dom_setstate(False, verbose) + + +del requires_id_in_link_range +del requires_id_in_dom_range +del requires_parsed + + +class YamlTopology(Topology): + """ 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. """ + + def __init__(self, path, conn): + super().__init__(conn) + self.path = path self._parse() def _parse(self): @@ -74,3 +243,5 @@ class YamlTopology: ) for dom in sorted_dom_names ] + + self.parsed = True diff --git a/spawn_network.py b/spawn_network.py index 0ca93b2..2f1775a 100755 --- a/spawn_network.py +++ b/spawn_network.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 from lxc_net import parse_network -import sys import signal import libvirt import argparse -import asyncio def parse_args(): @@ -16,18 +14,6 @@ def parse_args(): return args -async def spawn_network(link): - await link.async_create() - print(link.name, end="... ") - sys.stdout.flush() - - -async def spawn_networks(links): - link_tasks = [asyncio.create_task(spawn_network(link)) for link in links] - for link_task in link_tasks: - await link_task - - def main(): args = parse_args() @@ -46,25 +32,16 @@ def main(): topology = parse_network.YamlTopology(args.topology, conn) - print(">> Spawning networks: ", end="") - sys.stdout.flush() - asyncio.run(spawn_networks(topology.links)) - 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.") + topology.net_start(verbose=True) + topology.dom_start(verbose=True) print("Network running. Press ^C to terminate.") while not received_sigint: # Wait for SIGINT signal.pause() + topology.dom_stop(verbose=True) + topology.net_stop(verbose=True) + if __name__ == "__main__": main()