Compare commits

...
3 Commits
Author SHA1 Message Date
Josh Hawkins 5d42db3638 follow config symlinks in migration writes 2026-11-09 14:21:49 -06:00
Josh Hawkins 78a3174238 keep config owner and handle file bind mounts in migration writes
os.replace failed with EBUSY when config.yml was a file bind mount, as in the documented --validate-config command, and the replaced file lost its original owner when Frigate ran as root. The write now copies in place on EBUSY and restores the original owner when permitted. Tests cover a failed dump leaving the original config and the bind-mount fallback.
2026-11-09 14:11:04 -06:00
Josh Hawkins c30cda9ff2 write migrated config atomically
Config migration rewrote config.yml in place with open("w") after each step, so nginx's get_nginx_settings.py, which starts alongside frigate, could read an empty file, get None from yaml.load, and crash on config.get. Each step now writes to a temp file with the original mode and os.replace's it into place.
2026-11-09 13:07:31 -06:00
2 changed files with 70 additions and 18 deletions
+37
View File
@@ -1,5 +1,6 @@
"""Tests for migrating detectors and model into the models list."""
import errno
import logging
import os
import tempfile
@@ -240,6 +241,42 @@ class TestMigrateConfigFile(unittest.TestCase):
with open(self.config_file) as f:
return YAML().load(f)
def test_a_failed_write_leaves_the_original_config(self):
original = "mqtt:\n enabled: false\ncameras: {}\n"
with open(self.config_file, "w") as f:
f.write(original)
with (
patch.object(YAML, "dump", side_effect=RuntimeError),
self.assertRaises(RuntimeError),
):
migrate_frigate_config(self.config_file)
with open(self.config_file) as f:
self.assertEqual(f.read(), original)
def test_a_file_bind_mount_is_written_in_place(self):
with patch(
"frigate.util.config.os.replace",
side_effect=OSError(errno.EBUSY, "Device or resource busy"),
):
migrated = self._migrate("mqtt:\n enabled: false\ncameras: {}\n")
self.assertEqual(migrated["version"], CURRENT_CONFIG_VERSION)
self.assertFalse(os.path.exists(f"{self.config_file}.tmp"))
def test_a_symlinked_config_migrates_its_target(self):
target = os.path.join(self.temp_dir.name, "frigate.yml")
with open(target, "w") as f:
f.write("mqtt:\n enabled: false\ncameras: {}\n")
os.symlink(target, self.config_file)
migrate_frigate_config(self.config_file)
self.assertTrue(os.path.islink(self.config_file))
with open(target) as f:
self.assertEqual(YAML().load(f)["version"], CURRENT_CONFIG_VERSION)
def test_migrates_a_config_already_stamped_with_the_current_version(self):
# 0.19 is unreleased, so a dev config can be current and still use
# the pre-models keys
+33 -18
View File
@@ -1,6 +1,8 @@
"""configuration utils."""
import asyncio
import contextlib
import errno
import ipaddress
import logging
import os
@@ -212,6 +214,28 @@ def find_config_file() -> str:
return config_path
def _write_config(config_file: str, yaml: YAML, config: dict[str, Any]) -> None:
config_file = os.path.realpath(config_file)
st = os.stat(config_file)
tmp_file = f"{config_file}.tmp"
with open(tmp_file, "w") as f:
yaml.dump(config, f)
os.chmod(tmp_file, st.st_mode)
with contextlib.suppress(PermissionError):
os.chown(tmp_file, st.st_uid, st.st_gid)
try:
os.replace(tmp_file, config_file)
except OSError as e:
if e.errno != errno.EBUSY:
raise
# a file bind mount can't be renamed over, so write it in place
shutil.copyfile(tmp_file, config_file)
os.remove(tmp_file)
def migrate_frigate_config(config_file: str):
"""handle migrating the frigate config."""
logger.info("Checking if frigate config needs migration...")
@@ -259,8 +283,7 @@ def migrate_frigate_config(config_file: str):
if previous_version < "0.14":
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
new_config = migrate_014(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.14"
logger.info("Migrating export file names...")
@@ -277,56 +300,48 @@ def migrate_frigate_config(config_file: str):
if previous_version < "0.15-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...")
new_config = migrate_015_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.15-0"
if previous_version < "0.15-1":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...")
new_config = migrate_015_1(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.15-1"
if previous_version < "0.16-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...")
new_config = migrate_016_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.16-0"
if previous_version < "0.17-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...")
new_config = migrate_017_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.17-0"
if previous_version < "0.18-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.18-0...")
new_config = migrate_018_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.18-0"
if previous_version < "0.19-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.19-0...")
new_config = migrate_019_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.19-0"
if needs_models:
logger.info("Migrating frigate detectors and model to models...")
new_config = migrate_models(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
if needs_detector_rename:
logger.info("Migrating renamed frigate detectors...")
new_config = rename_hailo_detector(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
logger.info("Finished frigate config migration...")