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