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 concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from . import depsolver
|
||||||
from . import network
|
from . import network
|
||||||
from . import repo_local
|
from . import repo_local
|
||||||
from .exceptions import InvalidContainerStateError
|
from .exceptions import InvalidContainerStateError
|
||||||
@ -143,7 +144,13 @@ class Container:
|
|||||||
network.release_ip(self.name)
|
network.release_ip(self.name)
|
||||||
|
|
||||||
def start(self):
|
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])
|
subprocess.Popen(['lxc-start', '-P', CONTAINERS_DIR, self.name])
|
||||||
self.await_state(STATE_RUNNING)
|
self.await_state(STATE_RUNNING)
|
||||||
# Launch the readiness check in a separate thread, so it can be reliably cancelled after timeout
|
# 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)
|
time.sleep(0.25)
|
||||||
|
|
||||||
def stop(self):
|
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])
|
subprocess.Popen(['lxc-stop', '-P', CONTAINERS_DIR, self.name])
|
||||||
self.await_state(STATE_STOPPED)
|
self.await_state(STATE_STOPPED)
|
||||||
|
|
||||||
@ -204,3 +217,19 @@ class Container:
|
|||||||
if group:
|
if group:
|
||||||
gid = self.execute(['/usr/bin/getent', 'group', group], capture_output=True, check=True).stdout.decode().split(':')[2]
|
gid = self.execute(['/usr/bin/getent', 'group', group], capture_output=True, check=True).stdout.decode().split(':')[2]
|
||||||
return (uid,gid)
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
class CircularDependencyError(Exception):
|
from .exceptions import CircularDependencyError
|
||||||
pass
|
|
||||||
|
|
||||||
class Node:
|
class Node:
|
||||||
def __init__(self, name, depends):
|
def __init__(self, name, depends, instance):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.depends = set(depends)
|
self.depends = set(depends)
|
||||||
|
self.instance = instance
|
||||||
|
|
||||||
# "Batches" are sets of tasks that can be run together
|
def solve(nodes):
|
||||||
def solve_batches(nodes):
|
# Returns a list of instances ordered by dependency
|
||||||
# Build a map of node names to node instances
|
deps = {node.name: node for node in nodes}
|
||||||
name_to_instance = {n.name: n for n in nodes}
|
result = []
|
||||||
# Build a map of node names to dependency names
|
while deps:
|
||||||
name_to_deps = {n.name: n.depends.copy() for n in nodes}
|
# Get a batch of nodes not depending on anything (or originally depending on already resolved nodes)
|
||||||
# This is where we'll store the batches
|
batch = {name for name, node in deps.items() if not node.depends}
|
||||||
batches = []
|
if not batch:
|
||||||
# While there are dependencies to solve...
|
# If there are no such nodes, we have found a circular dependency
|
||||||
while name_to_deps:
|
raise CircularDependencyError(deps)
|
||||||
# Get all nodes with no dependencies
|
# Add instances tied to the resolved keys to the result and remove resolved keys from the dependecy map
|
||||||
ready = {name for name, deps in name_to_deps.items() if not deps}
|
for name in batch:
|
||||||
# If there aren't any, we have a loop in the graph
|
result.append(deps[name].instance)
|
||||||
if not ready:
|
del deps[name]
|
||||||
raise CircularDependencyError(name_to_deps)
|
# Remove resolved keys from the dependencies of yet unresolved nodes
|
||||||
# Remove them from the dependency graph
|
for node in deps.values():
|
||||||
for name in ready:
|
node.depends -= batch
|
||||||
del name_to_deps[name]
|
return result
|
||||||
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)
|
|
||||||
|
@ -29,3 +29,13 @@ class InvalidContainerStateError(Exception):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'Container "{self.container_name}" reached unexpected state {self.container_state}'
|
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