mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-11-09 10:59:43 +00:00
Add support for ulimit in addon config (#6206)
* Add support for ulimit in addon config Similar to docker-compose, this adds support for setting ulimits for addons via the addon config. This is useful e.g. for InfluxDB which on its own does not support setting higher open file descriptor limits, but recommends increasing limits on the host. * Make soft and hard limit mandatory if ulimit is a dict
This commit is contained in:
@@ -72,6 +72,7 @@ from ..const import (
|
||||
ATTR_TYPE,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_ULIMITS,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_VERSION,
|
||||
@@ -462,6 +463,11 @@ class AddonModel(JobGroup, ABC):
|
||||
"""Return True if the add-on have his own udev."""
|
||||
return self.data[ATTR_UDEV]
|
||||
|
||||
@property
|
||||
def ulimits(self) -> dict[str, Any]:
|
||||
"""Return ulimits configuration."""
|
||||
return self.data[ATTR_ULIMITS]
|
||||
|
||||
@property
|
||||
def with_kernel_modules(self) -> bool:
|
||||
"""Return True if the add-on access to kernel modules."""
|
||||
|
||||
@@ -88,6 +88,7 @@ from ..const import (
|
||||
ATTR_TYPE,
|
||||
ATTR_UART,
|
||||
ATTR_UDEV,
|
||||
ATTR_ULIMITS,
|
||||
ATTR_URL,
|
||||
ATTR_USB,
|
||||
ATTR_USER,
|
||||
@@ -423,6 +424,20 @@ _SCHEMA_ADDON_CONFIG = vol.Schema(
|
||||
False,
|
||||
),
|
||||
vol.Optional(ATTR_IMAGE): docker_image,
|
||||
vol.Optional(ATTR_ULIMITS, default=dict): vol.Any(
|
||||
{str: vol.Coerce(int)}, # Simple format: {name: limit}
|
||||
{
|
||||
str: vol.Any(
|
||||
vol.Coerce(int), # Simple format for individual entries
|
||||
vol.Schema(
|
||||
{ # Detailed format for individual entries
|
||||
vol.Required("soft"): vol.Coerce(int),
|
||||
vol.Required("hard"): vol.Coerce(int),
|
||||
}
|
||||
),
|
||||
)
|
||||
},
|
||||
),
|
||||
vol.Optional(ATTR_TIMEOUT, default=10): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=10, max=300)
|
||||
),
|
||||
|
||||
@@ -348,6 +348,7 @@ ATTR_TRANSLATIONS = "translations"
|
||||
ATTR_TYPE = "type"
|
||||
ATTR_UART = "uart"
|
||||
ATTR_UDEV = "udev"
|
||||
ATTR_ULIMITS = "ulimits"
|
||||
ATTR_UNHEALTHY = "unhealthy"
|
||||
ATTR_UNSAVED = "unsaved"
|
||||
ATTR_UNSUPPORTED = "unsupported"
|
||||
|
||||
@@ -318,7 +318,18 @@ class DockerAddon(DockerInterface):
|
||||
mem = 128 * 1024 * 1024
|
||||
limits.append(docker.types.Ulimit(name="memlock", soft=mem, hard=mem))
|
||||
|
||||
# Return None if no capabilities is present
|
||||
# Add configurable ulimits from add-on config
|
||||
for name, config in self.addon.ulimits.items():
|
||||
if isinstance(config, int):
|
||||
# Simple format: both soft and hard limits are the same
|
||||
limits.append(docker.types.Ulimit(name=name, soft=config, hard=config))
|
||||
elif isinstance(config, dict):
|
||||
# Detailed format: both soft and hard limits are mandatory
|
||||
soft = config["soft"]
|
||||
hard = config["hard"]
|
||||
limits.append(docker.types.Ulimit(name=name, soft=soft, hard=hard))
|
||||
|
||||
# Return None if no ulimits are present
|
||||
if limits:
|
||||
return limits
|
||||
return None
|
||||
|
||||
@@ -419,3 +419,71 @@ def test_valid_schema():
|
||||
config["schema"] = {"field": "invalid"}
|
||||
with pytest.raises(vol.Invalid):
|
||||
assert vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
|
||||
def test_ulimits_simple_format():
|
||||
"""Test ulimits simple format validation."""
|
||||
config = load_json_fixture("basic-addon-config.json")
|
||||
|
||||
config["ulimits"] = {"nofile": 65535, "nproc": 32768, "memlock": 134217728}
|
||||
|
||||
valid_config = vd.SCHEMA_ADDON_CONFIG(config)
|
||||
assert valid_config["ulimits"]["nofile"] == 65535
|
||||
assert valid_config["ulimits"]["nproc"] == 32768
|
||||
assert valid_config["ulimits"]["memlock"] == 134217728
|
||||
|
||||
|
||||
def test_ulimits_detailed_format():
|
||||
"""Test ulimits detailed format validation."""
|
||||
config = load_json_fixture("basic-addon-config.json")
|
||||
|
||||
config["ulimits"] = {
|
||||
"nofile": {"soft": 20000, "hard": 40000},
|
||||
"nproc": 32768, # Mixed format should work
|
||||
"memlock": {"soft": 67108864, "hard": 134217728},
|
||||
}
|
||||
|
||||
valid_config = vd.SCHEMA_ADDON_CONFIG(config)
|
||||
assert valid_config["ulimits"]["nofile"]["soft"] == 20000
|
||||
assert valid_config["ulimits"]["nofile"]["hard"] == 40000
|
||||
assert valid_config["ulimits"]["nproc"] == 32768
|
||||
assert valid_config["ulimits"]["memlock"]["soft"] == 67108864
|
||||
assert valid_config["ulimits"]["memlock"]["hard"] == 134217728
|
||||
|
||||
|
||||
def test_ulimits_empty_dict():
|
||||
"""Test ulimits with empty dict (default)."""
|
||||
config = load_json_fixture("basic-addon-config.json")
|
||||
|
||||
valid_config = vd.SCHEMA_ADDON_CONFIG(config)
|
||||
assert valid_config["ulimits"] == {}
|
||||
|
||||
|
||||
def test_ulimits_invalid_values():
|
||||
"""Test ulimits with invalid values."""
|
||||
config = load_json_fixture("basic-addon-config.json")
|
||||
|
||||
# Invalid string values
|
||||
config["ulimits"] = {"nofile": "invalid"}
|
||||
with pytest.raises(vol.Invalid):
|
||||
vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
# Invalid detailed format
|
||||
config["ulimits"] = {"nofile": {"invalid_key": 1000}}
|
||||
with pytest.raises(vol.Invalid):
|
||||
vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
# Missing hard value in detailed format
|
||||
config["ulimits"] = {"nofile": {"soft": 1000}}
|
||||
with pytest.raises(vol.Invalid):
|
||||
vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
# Missing soft value in detailed format
|
||||
config["ulimits"] = {"nofile": {"hard": 1000}}
|
||||
with pytest.raises(vol.Invalid):
|
||||
vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
# Empty dict in detailed format
|
||||
config["ulimits"] = {"nofile": {}}
|
||||
with pytest.raises(vol.Invalid):
|
||||
vd.SCHEMA_ADDON_CONFIG(config)
|
||||
|
||||
@@ -503,3 +503,93 @@ async def test_addon_new_device_no_haos(
|
||||
await install_addon_ssh.stop()
|
||||
assert coresys.resolution.issues == []
|
||||
assert coresys.resolution.suggestions == []
|
||||
|
||||
|
||||
async def test_ulimits_integration(
|
||||
coresys: CoreSys,
|
||||
install_addon_ssh: Addon,
|
||||
):
|
||||
"""Test ulimits integration with Docker addon."""
|
||||
docker_addon = DockerAddon(coresys, install_addon_ssh)
|
||||
|
||||
# Test default case (no ulimits, no realtime)
|
||||
assert docker_addon.ulimits is None
|
||||
|
||||
# Test with realtime enabled (should have built-in ulimits)
|
||||
install_addon_ssh.data["realtime"] = True
|
||||
ulimits = docker_addon.ulimits
|
||||
assert ulimits is not None
|
||||
assert len(ulimits) == 2
|
||||
# Check for rtprio limit
|
||||
rtprio_limit = next((u for u in ulimits if u.name == "rtprio"), None)
|
||||
assert rtprio_limit is not None
|
||||
assert rtprio_limit.soft == 90
|
||||
assert rtprio_limit.hard == 99
|
||||
# Check for memlock limit
|
||||
memlock_limit = next((u for u in ulimits if u.name == "memlock"), None)
|
||||
assert memlock_limit is not None
|
||||
assert memlock_limit.soft == 128 * 1024 * 1024
|
||||
assert memlock_limit.hard == 128 * 1024 * 1024
|
||||
|
||||
# Test with configurable ulimits (simple format)
|
||||
install_addon_ssh.data["realtime"] = False
|
||||
install_addon_ssh.data["ulimits"] = {"nofile": 65535, "nproc": 32768}
|
||||
ulimits = docker_addon.ulimits
|
||||
assert ulimits is not None
|
||||
assert len(ulimits) == 2
|
||||
|
||||
nofile_limit = next((u for u in ulimits if u.name == "nofile"), None)
|
||||
assert nofile_limit is not None
|
||||
assert nofile_limit.soft == 65535
|
||||
assert nofile_limit.hard == 65535
|
||||
|
||||
nproc_limit = next((u for u in ulimits if u.name == "nproc"), None)
|
||||
assert nproc_limit is not None
|
||||
assert nproc_limit.soft == 32768
|
||||
assert nproc_limit.hard == 32768
|
||||
|
||||
# Test with configurable ulimits (detailed format)
|
||||
install_addon_ssh.data["ulimits"] = {
|
||||
"nofile": {"soft": 20000, "hard": 40000},
|
||||
"memlock": {"soft": 67108864, "hard": 134217728},
|
||||
}
|
||||
ulimits = docker_addon.ulimits
|
||||
assert ulimits is not None
|
||||
assert len(ulimits) == 2
|
||||
|
||||
nofile_limit = next((u for u in ulimits if u.name == "nofile"), None)
|
||||
assert nofile_limit is not None
|
||||
assert nofile_limit.soft == 20000
|
||||
assert nofile_limit.hard == 40000
|
||||
|
||||
memlock_limit = next((u for u in ulimits if u.name == "memlock"), None)
|
||||
assert memlock_limit is not None
|
||||
assert memlock_limit.soft == 67108864
|
||||
assert memlock_limit.hard == 134217728
|
||||
|
||||
# Test mixed format and realtime (realtime + custom ulimits)
|
||||
install_addon_ssh.data["realtime"] = True
|
||||
install_addon_ssh.data["ulimits"] = {
|
||||
"nofile": 65535,
|
||||
"core": {"soft": 0, "hard": 0}, # Disable core dumps
|
||||
}
|
||||
ulimits = docker_addon.ulimits
|
||||
assert ulimits is not None
|
||||
assert (
|
||||
len(ulimits) == 4
|
||||
) # rtprio, memlock (from realtime) + nofile, core (from config)
|
||||
|
||||
# Check realtime limits still present
|
||||
rtprio_limit = next((u for u in ulimits if u.name == "rtprio"), None)
|
||||
assert rtprio_limit is not None
|
||||
|
||||
# Check custom limits added
|
||||
nofile_limit = next((u for u in ulimits if u.name == "nofile"), None)
|
||||
assert nofile_limit is not None
|
||||
assert nofile_limit.soft == 65535
|
||||
assert nofile_limit.hard == 65535
|
||||
|
||||
core_limit = next((u for u in ulimits if u.name == "core"), None)
|
||||
assert core_limit is not None
|
||||
assert core_limit.soft == 0
|
||||
assert core_limit.hard == 0
|
||||
|
||||
Reference in New Issue
Block a user