From 1c889fcaac0a9e05623ec47a9740e1eb54614894 Mon Sep 17 00:00:00 2001 From: Disassembler Date: Tue, 11 Feb 2020 15:39:21 +0100 Subject: [PATCH] Start / stop containers including dependencies --- usr/lib/python3.8/spoc/container.py | 33 ++++++++++- usr/lib/python3.8/spoc/depsolver.py | 86 +++++++--------------------- usr/lib/python3.8/spoc/exceptions.py | 10 ++++ 3 files changed, 62 insertions(+), 67 deletions(-) diff --git a/usr/lib/python3.8/spoc/container.py b/usr/lib/python3.8/spoc/container.py index 1193eb7..81d22ee 100644 --- a/usr/lib/python3.8/spoc/container.py +++ b/usr/lib/python3.8/spoc/container.py @@ -8,6 +8,7 @@ import time from concurrent.futures import ThreadPoolExecutor +from . import depsolver from . import network from . import repo_local from .exceptions import InvalidContainerStateError @@ -143,7 +144,13 @@ class Container: network.release_ip(self.name) def start(self): - # Start the container, wait until it is reported as started and execute application readiness check + # Start the container including its dependencies + for dependency in depsolver.solve(self.get_dependency_nodes()): + if dependency.get_state() != STATE_RUNNING: + dependency.do_start() + + def do_start(self): + # Start the current container, wait until it is reported as started and execute application readiness check subprocess.Popen(['lxc-start', '-P', CONTAINERS_DIR, self.name]) self.await_state(STATE_RUNNING) # Launch the readiness check in a separate thread, so it can be reliably cancelled after timeout @@ -167,7 +174,13 @@ class Container: time.sleep(0.25) def stop(self): - # Stop the container and wait until it stops completely + # Stop the containers depending on the current cotnainer + for dependency in depsolver.solve(self.get_reverse_dependency_nodes()): + if dependency.get_state() != STATE_STOPPED: + dependency.do_stop() + + def do_stop(self): + # Stop the current container and wait until it stops completely subprocess.Popen(['lxc-stop', '-P', CONTAINERS_DIR, self.name]) self.await_state(STATE_STOPPED) @@ -204,3 +217,19 @@ class Container: if group: gid = self.execute(['/usr/bin/getent', 'group', group], capture_output=True, check=True).stdout.decode().split(':')[2] return (uid,gid) + + def get_dependency_nodes(self): + nodes = [depsolver.Node(self.name, self.depends, self)] + for dependency in self.depends: + nodes.extend(Container(dependency).get_dependency_nodes()) + return nodes + + def get_reverse_dependency_nodes(self): + reverse_depends = [] + for name, definition in repo_local.get_containers().items(): + if 'depends' in definition and self.name in definition['depends']: + reverse_depends.append(name) + nodes = [depsolver.Node(self.name, reverse_depends, self)] + for dependency in reverse_depends: + nodes.extend(Container(dependency).get_reverse_dependency_nodes()) + return nodes diff --git a/usr/lib/python3.8/spoc/depsolver.py b/usr/lib/python3.8/spoc/depsolver.py index 44a7f4f..7d07b90 100644 --- a/usr/lib/python3.8/spoc/depsolver.py +++ b/usr/lib/python3.8/spoc/depsolver.py @@ -1,72 +1,28 @@ # -*- coding: utf-8 -*- -class CircularDependencyError(Exception): - pass +from .exceptions import CircularDependencyError class Node: - def __init__(self, name, depends): + def __init__(self, name, depends, instance): self.name = name self.depends = set(depends) + self.instance = instance -# "Batches" are sets of tasks that can be run together -def solve_batches(nodes): - # Build a map of node names to node instances - name_to_instance = {n.name: n for n in nodes} - # Build a map of node names to dependency names - name_to_deps = {n.name: n.depends.copy() for n in nodes} - # This is where we'll store the batches - batches = [] - # While there are dependencies to solve... - while name_to_deps: - # Get all nodes with no dependencies - ready = {name for name, deps in name_to_deps.items() if not deps} - # If there aren't any, we have a loop in the graph - if not ready: - raise CircularDependencyError(name_to_deps) - # Remove them from the dependency graph - for name in ready: - del name_to_deps[name] - for deps in name_to_deps.values(): - deps.difference_update(ready) - # Add the batch to the list - batches.append(ready) - return batches - -def solve_flat(nodes): - batches = solve_batches(nodes) - return [i for b in batches for i in b] - - - -a = Node("a", []) -b = Node("b", []) -c = Node("c", ["a"]) -d = Node("d", ["b"]) -e = Node("e", ["c", "d"]) -f = Node("f", ["a", "b"]) -g = Node("g", ["e", "f"]) -h = Node("h", ["g"]) -i = Node("i", ["a"]) -j = Node("j", ["b"]) -k = Node("k", []) -nodes = (a, b, c, d, e, f, g, h, i, j, k) - -# Show the batches on screen -print "Batches:" -for bundle in get_task_batches(nodes): - print ", ".join(node.name for node in bundle) -print - -# An example, *broken* dependency graph -a = Task("a", "i") -nodes = (a, b, c, d, e, f, g, h, i, j) - -# Show it on screen -print "A broken dependency graph example:" -print format_nodes(nodes) -print - -# This should raise an exception and show the current state of the graph -print "Trying to resolve the dependencies will raise an exception:" -print -get_task_batches(nodes) \ No newline at end of file +def solve(nodes): + # Returns a list of instances ordered by dependency + deps = {node.name: node for node in nodes} + result = [] + while deps: + # Get a batch of nodes not depending on anything (or originally depending on already resolved nodes) + batch = {name for name, node in deps.items() if not node.depends} + if not batch: + # If there are no such nodes, we have found a circular dependency + raise CircularDependencyError(deps) + # Add instances tied to the resolved keys to the result and remove resolved keys from the dependecy map + for name in batch: + result.append(deps[name].instance) + del deps[name] + # Remove resolved keys from the dependencies of yet unresolved nodes + for node in deps.values(): + node.depends -= batch + return result diff --git a/usr/lib/python3.8/spoc/exceptions.py b/usr/lib/python3.8/spoc/exceptions.py index 9ea90b4..c0c57b4 100644 --- a/usr/lib/python3.8/spoc/exceptions.py +++ b/usr/lib/python3.8/spoc/exceptions.py @@ -29,3 +29,13 @@ class InvalidContainerStateError(Exception): def __str__(self): return f'Container "{self.container_name}" reached unexpected state {self.container_state}' + +class CircularDependencyError(Exception): + # Dependecy solver has found a circular dependency between nodes + def __init__(self, deps): + self.deps = deps + + def __str__(self): + result = ['Dependency resolution failed due to circular dependency. Dumping unresolved dependencies:'] + result.extend(f'{dep} => {node.depends}' for dep, node in self.deps.items()) + return '\n'.join(result)