home-assistant: reset permissions when copying default blueprints (#416034)

This fixes the import of backups, that would break when they wanted to
nuke the existing config, because they had to npermission to delete the
default blueprints that were copied without write-permissions from the
nix store.

Changed files
+63 -10
nixos
pkgs
servers
+25 -10
nixos/tests/home-assistant.nix
···
];
};
lovelaceConfigWritable = true;
+
};
-
blueprints.automation = [
-
(pkgs.fetchurl {
-
url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml";
-
hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w=";
-
})
-
];
-
blueprints.template = [
-
"${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml"
-
];
+
# Add blueprints next, because we want to test the installation of the default blueprints first
+
specialisation.addBlueprints = {
+
inheritParentConfig = true;
+
configuration.services.home-assistant = {
+
blueprints.automation = [
+
(pkgs.fetchurl {
+
url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml";
+
hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w=";
+
})
+
];
+
blueprints.template = [
+
"${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml"
+
];
+
};
};
# Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
···
with subtest("Check extra components are considered in systemd unit hardening"):
hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
-
with subtest("Check that blueprints are installed"):
+
with subtest("Check that default blueprints are copied writable"):
+
hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant | grep 700")
+
hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant/motion_light.yaml | grep 600")
+
# Delete blueprints, so we can check the declarative setup next
+
hass.execute("rm -rf ${configDir}/blueprints")
+
+
with subtest("Check that configured blueprints are installed"):
+
cursor = get_journal_cursor()
+
hass.succeed("${system}/specialisation/addBlueprints/bin/switch-to-configuration test")
+
wait_for_homeassistant(cursor)
hass.succeed("test -L '${configDir}/blueprints/automation/motion_light.yaml'")
hass.succeed("test -L '${configDir}/blueprints/template/inverted_binary_sensor.yaml'")
+3
pkgs/servers/home-assistant/default.nix
···
# Follow symlinks in /var/lib/hass/www
./patches/static-follow-symlinks.patch
+
# Copy default blueprints without preserving permissions
+
./patches/default-blueprint-permissions.patch
+
# Patch path to ffmpeg binary
(replaceVars ./patches/ffmpeg-path.patch {
ffmpeg = "${lib.getExe ffmpeg-headless}";
+35
pkgs/servers/home-assistant/patches/default-blueprint-permissions.patch
···
+
commit cca718816f66b0155602c974e6743e8ce49e367b
+
Author: Martin Weinelt <hexa@darmstadt.ccc.de>
+
Date: Thu Jun 12 03:37:27 2025 +0200
+
+
blueprints: copy default blueprints w/o permissions
+
+
There is no easy way to achieve this with shutil, because copytree() will
+
always call copystat() an ruin directory permissions.
+
+
diff --git a/homeassistant/components/blueprint/models.py b/homeassistant/components/blueprint/models.py
+
index 88052100259..a0b29ed3d01 100644
+
--- a/homeassistant/components/blueprint/models.py
+
+++ b/homeassistant/components/blueprint/models.py
+
@@ -378,9 +378,17 @@ class DomainBlueprints:
+
if self.blueprint_folder.exists():
+
return
+
+
- shutil.copytree(
+
- integration.file_path / BLUEPRINT_FOLDER,
+
- self.blueprint_folder / HOMEASSISTANT_DOMAIN,
+
- )
+
+ import os
+
+ os.makedirs(self.blueprint_folder / HOMEASSISTANT_DOMAIN)
+
+
+
+ import subprocess
+
+ subprocess.check_call([
+
+ "cp",
+
+ "--no-preserve=mode",
+
+ "--no-target-directory",
+
+ "--recursive",
+
+ str(integration.file_path / BLUEPRINT_FOLDER),
+
+ str(self.blueprint_folder / HOMEASSISTANT_DOMAIN),
+
+ ])
+
+
await self.hass.async_add_executor_job(populate)