Add restart policy evaluation (#3886)

* Add restart policy evaluation

* No container meta does not fail evaluation
This commit is contained in:
Mike Degatano 2022-09-22 03:16:33 -04:00 committed by GitHub
parent c24b811180
commit a5103cc329
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 428 additions and 3 deletions

View File

@ -28,6 +28,15 @@ class ContainerState(str, Enum):
UNKNOWN = "unknown"
class RestartPolicy(str, Enum):
"""Restart policy of container."""
NO = "no"
ON_FAILURE = "on-failure"
UNLESS_STOPPED = "unless-stopped"
ALWAYS = "always"
DBUS_PATH = "/run/dbus"
DBUS_VOLUME = {"bind": DBUS_PATH, "mode": "ro"}

View File

@ -35,7 +35,7 @@ from ..exceptions import (
)
from ..resolution.const import ContextType, IssueType, SuggestionType
from ..utils import process_lock
from .const import ContainerState
from .const import ContainerState, RestartPolicy
from .manager import CommandReturn
from .monitor import DockerContainerStateEvent
from .stats import DockerStats
@ -134,6 +134,15 @@ class DockerInterface(CoreSysAttributes):
"""Return True if a task is in progress."""
return self.lock.locked()
@property
def restart_policy(self) -> RestartPolicy | None:
"""Return restart policy of container."""
if "RestartPolicy" not in self.meta_host:
return None
policy = self.meta_host["RestartPolicy"].get("Name")
return policy if policy else RestartPolicy.NO
@property
def security_opt(self) -> list[str]:
"""Control security options."""

View File

@ -3,7 +3,7 @@ import logging
from ..const import DOCKER_NETWORK_MASK
from ..coresys import CoreSysAttributes
from .const import ENV_TIME, ENV_TOKEN
from .const import ENV_TIME, ENV_TOKEN, RestartPolicy
from .interface import DockerInterface
_LOGGER: logging.Logger = logging.getLogger(__name__)
@ -46,7 +46,7 @@ class DockerObserver(DockerInterface, CoreSysAttributes):
hostname=self.name.replace("_", "-"),
detach=True,
security_opt=self.security_opt,
restart_policy={"Name": "always"},
restart_policy={"Name": RestartPolicy.ALWAYS.value},
extra_hosts={"supervisor": self.sys_docker.network.supervisor},
environment={
ENV_TIME: self.sys_timezone,

View File

@ -45,6 +45,7 @@ class UnsupportedReason(str, Enum):
OS = "os"
OS_AGENT = "os_agent"
PRIVILEGED = "privileged"
RESTART_POLICY = "restart_policy"
SOFTWARE = "software"
SOURCE_MODS = "source_mods"
SUPERVISOR_VERSION = "supervisor_version"

View File

@ -0,0 +1,72 @@
"""Evaluation class for restart policy."""
from supervisor.docker.const import RestartPolicy
from supervisor.docker.interface import DockerInterface
from ...const import CoreState
from ...coresys import CoreSys
from ..const import UnsupportedReason
from .base import EvaluateBase
def setup(coresys: CoreSys) -> EvaluateBase:
"""Initialize evaluation-setup function."""
return EvaluateRestartPolicy(coresys)
class EvaluateRestartPolicy(EvaluateBase):
"""Evaluate restart policy of containers."""
def __init__(self, coresys: CoreSys) -> None:
"""Initialize the evaluation class."""
super().__init__(coresys)
self.coresys = coresys
self._containers: list[str] = []
@property
def reason(self) -> UnsupportedReason:
"""Return a UnsupportedReason enum."""
return UnsupportedReason.RESTART_POLICY
@property
def on_failure(self) -> str:
"""Return a string that is printed when self.evaluate is True."""
return f"Found containers with unsupported restart policy: {self._containers}"
@property
def states(self) -> list[CoreState]:
"""Return a list of valid states when this evaluation can run."""
return [CoreState.RUNNING]
@property
def no_restart_expected(self) -> set[DockerInterface]:
"""Docker interfaces where no restart is expected policy."""
return {
self.sys_supervisor.instance,
self.sys_homeassistant.core.instance,
*{
plug.instance
for plug in self.sys_plugins.all_plugins
if plug != self.sys_plugins.observer
},
*{addon.instance for addon in self.sys_addons.installed},
}
@property
def always_restart_expected(self) -> set[DockerInterface]:
"""Docker interfaces where always restart is expected policy."""
return {self.sys_plugins.observer.instance}
async def evaluate(self) -> bool:
"""Run evaluation, return true if system fails."""
self._containers = {
instance.name
for instance in self.no_restart_expected
if instance.restart_policy and instance.restart_policy != RestartPolicy.NO
} | {
instance.name
for instance in self.always_restart_expected
if instance.restart_policy
and instance.restart_policy != RestartPolicy.ALWAYS
}
return len(self._containers) > 0

253
tests/fixtures/container_attrs.json vendored Normal file
View File

@ -0,0 +1,253 @@
{
"Id": "986e5efadb228654f1719735e802fecc099136e4640155887946246a87fc584a",
"Created": "2022-09-21T18:54:13.269240742Z",
"Path": "/init",
"Args": [],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 2723,
"ExitCode": 0,
"Error": "",
"StartedAt": "2022-09-21T18:54:14.124021953Z",
"FinishedAt": "0001-01-01T00:00:00Z"
},
"Image": "sha256:bc34c81c040474bdc005e52857c04103702b081f58b70d5dbcdfc8261a03a4d9",
"ResolvConfPath": "/mnt/data/docker/containers/986e5efadb228654f1719735e802fecc099136e4640155887946246a87fc584a/resolv.conf",
"HostnamePath": "/mnt/data/docker/containers/986e5efadb228654f1719735e802fecc099136e4640155887946246a87fc584a/hostname",
"HostsPath": "/mnt/data/docker/containers/986e5efadb228654f1719735e802fecc099136e4640155887946246a87fc584a/hosts",
"LogPath": "",
"Name": "/addon_core_mosquitto",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "docker-default",
"ExecIDs": null,
"HostConfig": {
"Binds": [
"/dev:/dev:ro",
"/mnt/data/supervisor/addons/data/core_mosquitto:/data:rw",
"/mnt/data/supervisor/ssl:/ssl:ro",
"/mnt/data/supervisor/share:/share:ro"
],
"ContainerIDFile": "",
"LogConfig": { "Type": "journald", "Config": { "tag": "{{.Name}}" } },
"NetworkMode": "default",
"PortBindings": { "1883/tcp": [{ "HostIp": "", "HostPort": "1883" }] },
"RestartPolicy": { "Name": "", "MaximumRetryCount": 0 },
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": ["172.30.32.3"],
"DnsOptions": null,
"DnsSearch": ["local.hass.io"],
"ExtraHosts": ["hassio:172.30.32.2", "supervisor:172.30.32.2"],
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 200,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": ["seccomp=unconfined"],
"Tmpfs": { "/dev/shm": "" },
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"ConsoleSize": [0, 0],
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": null,
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": null,
"DeviceCgroupRules": null,
"DeviceRequests": null,
"KernelMemory": 0,
"KernelMemoryTCP": 0,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
],
"Init": false
},
"GraphDriver": {
"Data": {
"LowerDir": "/mnt/data/docker/overlay2/1b79280bcdd879c0df64ee41755c70610cf3b650cea299c6f30081338bf745e5-init/diff:/mnt/data/docker/overlay2/2c22d6a27a68fb384f85d4bb833f7ac7956fcbb93be9f731e6deb76bb9420011/diff:/mnt/data/docker/overlay2/42ce3dc994391b83f90bf599f887a8e0fdc2ee24cec9b14f491412f1dae11714/diff:/mnt/data/docker/overlay2/9f7c5f5a2aa9a324c75f9c54fbee648bee86d963c4ad6fde2cb3ca0d3f886287/diff:/mnt/data/docker/overlay2/8a4cd2bb72bb673a42186a1b94aaf1bbba1f1e35953fea91e6ccc2c08b0c7dc8/diff:/mnt/data/docker/overlay2/eb50a9462c19acb312d44e3455579f84f3ef256e410c4992554f1527fbd2b9dc/diff:/mnt/data/docker/overlay2/d38688df678f243a89c055f768b5ec80f1ce0686ed28940fcc4430edc5b311a0/diff",
"MergedDir": "/mnt/data/docker/overlay2/1b79280bcdd879c0df64ee41755c70610cf3b650cea299c6f30081338bf745e5/merged",
"UpperDir": "/mnt/data/docker/overlay2/1b79280bcdd879c0df64ee41755c70610cf3b650cea299c6f30081338bf745e5/diff",
"WorkDir": "/mnt/data/docker/overlay2/1b79280bcdd879c0df64ee41755c70610cf3b650cea299c6f30081338bf745e5/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "bind",
"Source": "/dev",
"Destination": "/dev",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/mnt/data/supervisor/addons/data/core_mosquitto",
"Destination": "/data",
"Mode": "rw",
"RW": true,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/mnt/data/supervisor/ssl",
"Destination": "/ssl",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
},
{
"Type": "bind",
"Source": "/mnt/data/supervisor/share",
"Destination": "/share",
"Mode": "ro",
"RW": false,
"Propagation": "rprivate"
}
],
"Config": {
"Hostname": "core-mosquitto",
"Domainname": "local.hass.io",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": { "1883/tcp": {} },
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"TZ=America/New_York",
"SUPERVISOR_TOKEN=abc123",
"HASSIO_TOKEN=abc123",
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"LANG=C.UTF-8",
"DEBIAN_FRONTEND=noninteractive",
"CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt",
"S6_BEHAVIOUR_IF_STAGE2_FAILS=2",
"S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0",
"S6_CMD_WAIT_FOR_SERVICES=1"
],
"Cmd": null,
"Image": "homeassistant/aarch64-addon-mosquitto:6.1.3",
"Volumes": { "/data": {}, "/dev": {}, "/share": {}, "/ssl": {} },
"WorkingDir": "/",
"Entrypoint": ["/init"],
"OnBuild": null,
"Labels": {
"io.hass.arch": "aarch64",
"io.hass.base.arch": "aarch64",
"io.hass.base.image": "arm64v8/debian:bullseye-slim",
"io.hass.base.name": "debian",
"io.hass.base.version": "2022.08.0",
"io.hass.description": "An Open Source MQTT broker",
"io.hass.name": "Mosquitto broker",
"io.hass.type": "addon",
"io.hass.url": "https://github.com/home-assistant/hassio-addons/tree/master/mosquitto",
"io.hass.version": "6.1.3",
"org.opencontainers.image.created": "2022-08-30 07:33:03+00:00",
"org.opencontainers.image.source": "https://github.com/home-assistant/docker-base",
"org.opencontainers.image.version": "6.1.3",
"supervisor_managed": ""
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "067cd11a63f96d227dcc0f01d3e4f5053c368021becd0b4b2da4f301cfda3d29",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"1883/tcp": [
{ "HostIp": "0.0.0.0", "HostPort": "1883" },
{ "HostIp": "::", "HostPort": "1883" }
]
},
"SandboxKey": "/var/run/docker/netns/067cd11a63f9",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "",
"Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "",
"IPPrefixLen": 0,
"IPv6Gateway": "",
"MacAddress": "",
"Networks": {
"hassio": {
"IPAMConfig": null,
"Links": null,
"Aliases": ["core-mosquitto", "986e5efadb22"],
"NetworkID": "cd10b1eb5f4a1cd5179839f81ccdadd29545eaa0b921454d1a2e0452c12d6935",
"EndpointID": "9b2c58f4595618241bb45df028c95f2713bb0b1b6326d3bdab2366e2caadbe7b",
"Gateway": "172.30.32.1",
"IPAddress": "172.30.33.1",
"IPPrefixLen": 23,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:1e:21:01",
"DriverOpts": null
}
}
}
}

View File

@ -0,0 +1,81 @@
"""Test evaluate restart policy.."""
from unittest.mock import MagicMock, patch
from awesomeversion import AwesomeVersion
from supervisor.addons.addon import Addon
from supervisor.const import CoreState
from supervisor.coresys import CoreSys
from supervisor.resolution.evaluations.restart_policy import EvaluateRestartPolicy
from tests.common import load_json_fixture
TEST_VERSION = AwesomeVersion("1.0.0")
async def test_evaluation(coresys: CoreSys, install_addon_ssh: Addon):
"""Test evaluation."""
restart_policy = EvaluateRestartPolicy(coresys)
coresys.core.state = CoreState.RUNNING
await restart_policy()
assert restart_policy.reason not in coresys.resolution.unsupported
no_restart_attrs = load_json_fixture("container_attrs.json")
always_restart_attrs = load_json_fixture("container_attrs.json")
always_restart_attrs["HostConfig"]["RestartPolicy"]["Name"] = "always"
addon_attrs = no_restart_attrs
observer_attrs = always_restart_attrs
def get_container(name: str):
meta = MagicMock()
meta.attrs = observer_attrs if name == "hassio_observer" else addon_attrs
return meta
coresys.docker.containers.get = get_container
await coresys.plugins.observer.instance.attach(TEST_VERSION)
await install_addon_ssh.instance.attach(TEST_VERSION)
await restart_policy()
assert restart_policy.reason not in coresys.resolution.unsupported
addon_attrs = always_restart_attrs
await install_addon_ssh.instance.attach(TEST_VERSION)
await restart_policy()
assert restart_policy.reason in coresys.resolution.unsupported
addon_attrs = no_restart_attrs
await install_addon_ssh.instance.attach(TEST_VERSION)
await restart_policy()
assert restart_policy.reason not in coresys.resolution.unsupported
observer_attrs = no_restart_attrs
await coresys.plugins.observer.instance.attach(TEST_VERSION)
await restart_policy()
assert restart_policy.reason in coresys.resolution.unsupported
async def test_did_run(coresys: CoreSys):
"""Test that the evaluation ran as expected."""
restart_policy = EvaluateRestartPolicy(coresys)
should_run = restart_policy.states
should_not_run = [state for state in CoreState if state not in should_run]
assert len(should_run) != 0
assert len(should_not_run) != 0
with patch(
"supervisor.resolution.evaluations.restart_policy.EvaluateRestartPolicy.evaluate",
return_value=False,
) as evaluate:
for state in should_run:
coresys.core.state = state
await restart_policy()
evaluate.assert_called_once()
evaluate.reset_mock()
for state in should_not_run:
coresys.core.state = state
await restart_policy()
evaluate.assert_not_called()
evaluate.reset_mock()