Start / stop containers including dependencies

This commit is contained in:
Disassembler 2020-02-11 15:39:21 +01:00
parent 81ecaed95e
commit 1c889fcaac
No known key found for this signature in database
GPG Key ID: 524BD33A0EE29499
3 changed files with 62 additions and 67 deletions

View File

@ -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

View File

@ -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)
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

View File

@ -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)