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 ensurePermissions = {
16 "DATABASE hass" = "ALL PRIVILEGES";
17 };
18 }];
19 };
20
21 services.home-assistant = {
22 enable = true;
23 inherit configDir;
24
25 # tests loading components by overriding the package
26 package = (pkgs.home-assistant.override {
27 extraPackages = ps: with ps; [
28 colorama
29 ];
30 extraComponents = [ "zha" ];
31 }).overrideAttrs (oldAttrs: {
32 doInstallCheck = false;
33 });
34
35 # tests loading components from the module
36 extraComponents = [
37 "wake_on_lan"
38 ];
39
40 # test extra package passing from the module
41 extraPackages = python3Packages: with python3Packages; [
42 psycopg2
43 ];
44
45 config = {
46 homeassistant = {
47 name = "Home";
48 time_zone = "UTC";
49 latitude = "0.0";
50 longitude = "0.0";
51 elevation = 0;
52 };
53
54 # configure the recorder component to use the postgresql db
55 recorder.db_url = "postgresql://@/hass";
56
57 # we can't load default_config, because the updater requires
58 # network access and would cause an error, so load frontend
59 # here explicitly.
60 # https://www.home-assistant.io/integrations/frontend/
61 frontend = {};
62
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/
66 switch = [ {
67 platform = "wake_on_lan";
68 mac = "00:11:22:33:44:55";
69 host = "127.0.0.1";
70 } ];
71
72 # test component-based capability assignment (CAP_NET_BIND_SERVICE)
73 # https://www.home-assistant.io/integrations/emulated_hue/
74 emulated_hue = {
75 host_ip = "127.0.0.1";
76 listen_port = 80;
77 };
78
79 # https://www.home-assistant.io/integrations/logger/
80 logger = {
81 default = "info";
82 };
83 };
84
85 # configure the sample lovelace dashboard
86 lovelaceConfig = {
87 title = "My Awesome Home";
88 views = [{
89 title = "Example";
90 cards = [{
91 type = "markdown";
92 title = "Lovelace";
93 content = "Welcome to your **Lovelace UI**.";
94 }];
95 }];
96 };
97 lovelaceConfigWritable = true;
98 };
99
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";
104 };
105
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 = {};
110 };
111 };
112
113 testScript = { nodes, ... }: let
114 system = nodes.hass.config.system.build.toplevel;
115 in
116 ''
117 import re
118 import json
119
120 start_all()
121
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)
128 assert match
129 package = match.group('path')
130
131
132 def get_journal_cursor(host) -> str:
133 exit, out = host.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
134 assert exit == 0
135 return json.loads(out)["__CURSOR"]
136
137
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'")
140
141
142 hass.wait_for_unit("home-assistant.service")
143 cursor = get_journal_cursor(hass)
144
145 with subtest("Check that YAML configuration file is in place"):
146 hass.succeed("test -L ${configDir}/configuration.yaml")
147
148 with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
149 hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
150
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")
154
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")
158
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")
163
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")
167
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")
170
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)
179
180 with subtest("check service restarts when package changes"):
181 pid = new_pid
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)
187
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
191
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"))
195 '';
196})