Start / stop containers including dependencies
This commit is contained in:
parent
81ecaed95e
commit
1c889fcaac
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user