at master 12 kB view raw
1{ 2 lib, 3 ... 4}: 5 6let 7 configDir = "/var/lib/foobar"; 8in 9{ 10 name = "home-assistant"; 11 meta.maintainers = lib.teams.home-assistant.members; 12 13 nodes.hass = 14 { pkgs, ... }: 15 { 16 services.postgresql = { 17 enable = true; 18 ensureDatabases = [ "hass" ]; 19 ensureUsers = [ 20 { 21 name = "hass"; 22 ensureDBOwnership = true; 23 } 24 ]; 25 }; 26 27 services.home-assistant = { 28 enable = true; 29 inherit configDir; 30 31 # provide dependencies through package overrides 32 package = ( 33 pkgs.home-assistant.override { 34 extraPackages = 35 ps: with ps; [ 36 colorama 37 ]; 38 extraComponents = [ 39 # test char-tty device allow propagation into the service 40 "zha" 41 ]; 42 } 43 ); 44 45 # provide component dependencies explicitly from the module 46 extraComponents = [ 47 "mqtt" 48 ]; 49 50 # provide package for postgresql support 51 extraPackages = 52 python3Packages: with python3Packages; [ 53 psycopg2 54 ]; 55 56 # test loading custom components 57 customComponents = with pkgs.home-assistant-custom-components; [ 58 prometheus_sensor 59 # tests loading multiple components from a single package 60 spook 61 ]; 62 63 # test loading lovelace modules 64 customLovelaceModules = with pkgs.home-assistant-custom-lovelace-modules; [ 65 mini-graph-card 66 ]; 67 68 config = { 69 homeassistant = { 70 name = "Home"; 71 time_zone = "UTC"; 72 latitude = "0.0"; 73 longitude = "0.0"; 74 elevation = 0; 75 }; 76 77 # configure the recorder component to use the postgresql db 78 recorder.db_url = "postgresql://@/hass"; 79 80 # we can't load default_config, because the updater requires 81 # network access and would cause an error, so load frontend 82 # here explicitly. 83 # https://www.home-assistant.io/integrations/frontend/ 84 frontend = { }; 85 86 # include some popular integrations, that absolutely shouldn't break 87 knx = { }; 88 zha = { }; 89 90 # set up a wake-on-lan switch to test capset capability required 91 # for the ping suid wrapper 92 # https://www.home-assistant.io/integrations/wake_on_lan/ 93 switch = [ 94 { 95 platform = "wake_on_lan"; 96 mac = "00:11:22:33:44:55"; 97 host = "127.0.0.1"; 98 } 99 ]; 100 101 # test component-based capability assignment (CAP_NET_BIND_SERVICE) 102 # https://www.home-assistant.io/integrations/emulated_hue/ 103 emulated_hue = { 104 host_ip = "127.0.0.1"; 105 listen_port = 80; 106 }; 107 108 # https://www.home-assistant.io/integrations/logger/ 109 logger = { 110 default = "debug"; 111 }; 112 }; 113 114 # configure the sample lovelace dashboard 115 lovelaceConfig = { 116 title = "My Awesome Home"; 117 views = [ 118 { 119 title = "Example"; 120 cards = [ 121 { 122 type = "markdown"; 123 title = "Lovelace"; 124 content = "Welcome to your **Lovelace UI**."; 125 } 126 ]; 127 } 128 ]; 129 }; 130 lovelaceConfigWritable = true; 131 }; 132 133 # Add blueprints next, because we want to test the installation of the default blueprints first 134 specialisation.addBlueprints = { 135 inheritParentConfig = true; 136 configuration.services.home-assistant = { 137 blueprints.automation = [ 138 (pkgs.fetchurl { 139 url = "https://github.com/home-assistant/core/raw/2025.1.4/homeassistant/components/automation/blueprints/motion_light.yaml"; 140 hash = "sha256-4HrDX65ycBMfEY2nZ7A25/d3ZnIHdpHZ+80Cblp+P5w="; 141 }) 142 ]; 143 blueprints.template = [ 144 "${pkgs.home-assistant.src}/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml" 145 ]; 146 }; 147 }; 148 149 # Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded. 150 specialisation.differentName = { 151 inheritParentConfig = true; 152 configuration.services.home-assistant.config.homeassistant.name = lib.mkForce "Test Home"; 153 }; 154 155 # Cause a configuration change that requires a service restart as we added a new runtime dependency 156 specialisation.newFeature = { 157 inheritParentConfig = true; 158 configuration.services.home-assistant.config.prometheus = { }; 159 }; 160 161 specialisation.removeCustomThings = { 162 inheritParentConfig = true; 163 configuration.services.home-assistant = { 164 customComponents = lib.mkForce [ ]; 165 customLovelaceModules = lib.mkForce [ ]; 166 blueprints.automation = lib.mkForce [ ]; 167 blueprints.template = lib.mkForce [ ]; 168 }; 169 }; 170 }; 171 172 testScript = 173 { nodes, ... }: 174 let 175 system = nodes.hass.system.build.toplevel; 176 in 177 '' 178 import json 179 180 start_all() 181 182 183 def get_journal_cursor() -> str: 184 exit, out = hass.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR") 185 assert exit == 0 186 return json.loads(out)["__CURSOR"] 187 188 189 def get_journal_since(cursor) -> str: 190 exit, out = hass.execute(f"journalctl --after-cursor='{cursor}' -u home-assistant.service") 191 assert exit == 0 192 return out 193 194 195 def get_unit_property(property) -> str: 196 exit, out = hass.execute(f"systemctl show --property={property} home-assistant.service") 197 assert exit == 0 198 return out 199 200 201 def wait_for_homeassistant(cursor): 202 hass.wait_until_succeeds(f"journalctl --after-cursor='{cursor}' -u home-assistant.service | grep -q 'Home Assistant initialized in'") 203 204 205 hass.wait_for_unit("home-assistant.service") 206 cursor = get_journal_cursor() 207 208 with subtest("Check that YAML configuration file is in place"): 209 hass.succeed("test -L ${configDir}/configuration.yaml") 210 211 with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"): 212 hass.succeed("test -f ${configDir}/ui-lovelace.yaml") 213 214 with subtest("Check that Home Assistant's web interface and API can be reached"): 215 wait_for_homeassistant(cursor) 216 hass.wait_for_open_port(8123) 217 hass.succeed("curl --fail http://localhost:8123/lovelace") 218 219 with subtest("Check that custom components get installed"): 220 hass.succeed("test -f ${configDir}/custom_components/prometheus_sensor/manifest.json") 221 for integration in ("prometheus_sensor", "spook", "spook_inverse"): 222 hass.wait_until_succeeds(f"journalctl -u home-assistant.service | grep -q 'We found a custom integration {integration} which has not been tested by Home Assistant'") 223 224 with subtest("Check that lovelace modules are referenced and fetchable"): 225 hass.succeed("grep -q 'mini-graph-card-bundle.js' '${configDir}/configuration.yaml'") 226 hass.succeed("curl --fail http://localhost:8123/local/nixos-lovelace-modules/mini-graph-card-bundle.js") 227 228 with subtest("Check that optional dependencies are in the PYTHONPATH"): 229 env = get_unit_property("Environment") 230 python_path = env.split("PYTHONPATH=")[1].split()[0] 231 for package in ["colorama", "paho-mqtt", "psycopg2"]: 232 assert package in python_path, f"{package} not in PYTHONPATH" 233 234 with subtest("Check that declaratively configured components get setup"): 235 journal = get_journal_since(cursor) 236 for domain in ["emulated_hue", "wake_on_lan"]: 237 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing" 238 239 with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"): 240 hass.wait_for_open_port(80) 241 hass.succeed("curl --fail http://localhost:80/description.xml") 242 243 with subtest("Check extra components are considered in systemd unit hardening"): 244 hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB") 245 246 with subtest("Check that default blueprints are copied writable"): 247 hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant | grep 700") 248 hass.succeed("stat -c '%a' ${configDir}/blueprints/automation/homeassistant/motion_light.yaml | grep 600") 249 # Delete blueprints, so we can check the declarative setup next 250 hass.execute("rm -rf ${configDir}/blueprints") 251 252 with subtest("Check that configured blueprints are installed"): 253 cursor = get_journal_cursor() 254 hass.succeed("${system}/specialisation/addBlueprints/bin/switch-to-configuration test") 255 wait_for_homeassistant(cursor) 256 hass.succeed("test -L '${configDir}/blueprints/automation/motion_light.yaml'") 257 hass.succeed("test -L '${configDir}/blueprints/template/inverted_binary_sensor.yaml'") 258 259 with subtest("Check service restart from SIGHUP"): 260 pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") 261 cursor = get_journal_cursor() 262 hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test") 263 wait_for_homeassistant(cursor) 264 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") 265 assert pid != new_pid, "The PID of the process must change after sending SIGHUP" 266 267 with subtest("Check service restarts when dependencies change"): 268 pid = new_pid 269 cursor = get_journal_cursor() 270 hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test") 271 wait_for_homeassistant(cursor) 272 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service") 273 assert pid != new_pid, "The PID of the process must change when its PYTHONPATH changess" 274 275 with subtest("Check that new components get setup after restart"): 276 journal = get_journal_since(cursor) 277 for domain in ["prometheus"]: 278 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing" 279 280 with subtest("Check custom components, custom lovelace modules, and blueprints get removed"): 281 cursor = get_journal_cursor() 282 hass.succeed("${system}/specialisation/removeCustomThings/bin/switch-to-configuration test") 283 hass.fail("grep -q 'mini-graph-card-bundle.js' '${configDir}/ui-lovelace.yaml'") 284 for integration in ("prometheus_sensor", "spook", "spook_inverse"): 285 hass.fail(f"test -f ${configDir}/custom_components/{integration}/manifest.json") 286 hass.fail("test -e '${configDir}/blueprints/automation/motion_light.yaml'") 287 hass.fail("test -e '${configDir}/blueprints/template/inverted_binary_sensor.yaml'") 288 wait_for_homeassistant(cursor) 289 290 with subtest("Check that no errors were logged"): 291 hass.fail("journalctl -u home-assistant -o cat | grep -q ERROR") 292 293 with subtest("Check systemd unit hardening"): 294 hass.log(hass.succeed("systemctl cat home-assistant.service")) 295 hass.log(hass.succeed("systemd-analyze security home-assistant.service")) 296 ''; 297}