1 import ./make-test-python.nix ({ pkgs, lib, ... }:
4 configDir = "/var/lib/foobar";
6 name = "home-assistant";
7 meta.maintainers = lib.teams.home-assistant.members;
9 nodes.hass = { pkgs, ... }: {
10 services.postgresql = {
12 ensureDatabases = [ "hass" ];
16 "DATABASE hass" = "ALL PRIVILEGES";
21 services.home-assistant = {
25 # tests loading components by overriding the package
26 package = (pkgs.home-assistant.override {
27 extraPackages = ps: with ps; [
30 extraComponents = [ "zha" ];
31 }).overrideAttrs (oldAttrs: {
32 doInstallCheck = false;
35 # tests loading components from the module
40 # test extra package passing from the module
41 extraPackages = python3Packages: with python3Packages; [
54 # configure the recorder component to use the postgresql db
55 recorder.db_url = "postgresql://@/hass";
57 # we can't load default_config, because the updater requires
58 # network access and would cause an error, so load frontend
60 # https://www.home-assistant.io/integrations/frontend/
63 # set up a wake-on-lan switch to test capset capability required
64 # for the ping suid wrapper
65 # https://www.home-assistant.io/integrations/wake_on_lan/
67 platform = "wake_on_lan";
68 mac = "00:11:22:33:44:55";
72 # test component-based capability assignment (CAP_NET_BIND_SERVICE)
73 # https://www.home-assistant.io/integrations/emulated_hue/
75 host_ip = "127.0.0.1";
79 # https://www.home-assistant.io/integrations/logger/
85 # configure the sample lovelace dashboard
87 title = "My Awesome Home";
93 content = "Welcome to your **Lovelace UI**.";
97 lovelaceConfigWritable = true;
100 # Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
101 specialisation.differentName = {
102 inheritParentConfig = true;
103 configuration.services.home-assistant.config.homeassistant.name = lib.mkForce "Test Home";
106 # Cause a configuration change that requires a service restart as we added a new runtime dependency
107 specialisation.newFeature = {
108 inheritParentConfig = true;
109 configuration.services.home-assistant.config.esphome = {};
113 testScript = { nodes, ... }: let
114 system = nodes.hass.config.system.build.toplevel;
122 # Parse the package path out of the systemd unit, as we cannot
123 # access the final package, that is overriden inside the module,
124 # by any other means.
125 pattern = re.compile(r"path=(?P<path>[\/a-z0-9-.]+)\/bin\/hass")
126 response = hass.execute("systemctl show -p ExecStart home-assistant.service")[1]
127 match = pattern.search(response)
129 package = match.group('path')
132 def get_journal_cursor(host) -> str:
133 exit, out = host.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
135 return json.loads(out)["__CURSOR"]
138 def wait_for_homeassistant(host, cursor):
139 host.wait_until_succeeds(f"journalctl --after-cursor='{cursor}' -u home-assistant.service | grep -q 'Home Assistant initialized in'")
142 hass.wait_for_unit("home-assistant.service")
143 cursor = get_journal_cursor(hass)
145 with subtest("Check that YAML configuration file is in place"):
146 hass.succeed("test -L ${configDir}/configuration.yaml")
148 with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
149 hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
151 with subtest("Check extraComponents and extraPackages are considered from the package"):
152 hass.succeed(f"grep -q 'colorama' {package}/extra_packages")
153 hass.succeed(f"grep -q 'zha' {package}/extra_components")
155 with subtest("Check extraComponents and extraPackages are considered from the module"):
156 hass.succeed(f"grep -q 'psycopg2' {package}/extra_packages")
157 hass.succeed(f"grep -q 'wake_on_lan' {package}/extra_components")
159 with subtest("Check that Home Assistant's web interface and API can be reached"):
160 wait_for_homeassistant(hass, cursor)
161 hass.wait_for_open_port(8123)
162 hass.succeed("curl --fail http://localhost:8123/lovelace")
164 with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
165 hass.wait_for_open_port(80)
166 hass.succeed("curl --fail http://localhost:80/description.xml")
168 with subtest("Check extra components are considered in systemd unit hardening"):
169 hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
171 with subtest("Check service reloads when configuration changes"):
172 # store the old pid of the process
173 pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
174 cursor = get_journal_cursor(hass)
175 hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test")
176 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
177 assert pid == new_pid, "The PID of the process should not change between process reloads"
178 wait_for_homeassistant(hass, cursor)
180 with subtest("check service restarts when package changes"):
182 cursor = get_journal_cursor(hass)
183 hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test")
184 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
185 assert pid != new_pid, "The PID of the process shoudl change when the HA binary changes"
186 wait_for_homeassistant(hass, cursor)
188 with subtest("Check that no errors were logged"):
189 output_log = hass.succeed("cat ${configDir}/home-assistant.log")
190 assert "ERROR" not in output_log
192 with subtest("Check systemd unit hardening"):
193 hass.log(hass.succeed("systemctl cat home-assistant.service"))
194 hass.log(hass.succeed("systemd-analyze security home-assistant.service"))