1# Run with:
2# cd nixpkgs
3# nix-build -A nixosTests.modular-service-etc
4
5# This tests the NixOS modular service integration to make sure `etc` entries
6# are generated correctly for `configData` files.
7{ lib, ... }:
8{
9 _class = "nixosTest";
10 name = "modular-service-etc";
11
12 nodes = {
13 server =
14 { pkgs, ... }:
15 let
16 # Normally the package services.default attribute combines this, but we
17 # don't have that, because this is not a production service. Should it be?
18 python-http-server = {
19 imports = [ ./python-http-server.nix ];
20 python-http-server.package = pkgs.python3;
21 };
22 in
23 {
24 system.services.webserver = {
25 # The python web server is simple enough that it doesn't need a reload signal.
26 # Other services may need to receive a signal in order to re-read what's in `configData`.
27 imports = [ python-http-server ];
28 python-http-server = {
29 port = 8080;
30 };
31
32 configData = {
33 "webroot" = {
34 source = pkgs.runCommand "webroot" { } ''
35 mkdir -p $out
36 cat > $out/index.html << 'EOF'
37 <!DOCTYPE html>
38 <html>
39 <head><title>Python Web Server</title></head>
40 <body>
41 <h1>Welcome to the Python Web Server</h1>
42 <p>Serving from port 8080</p>
43 </body>
44 </html>
45 EOF
46 '';
47 };
48 };
49
50 # Add a sub-service
51 services.api = {
52 imports = [ python-http-server ];
53 python-http-server = {
54 port = 8081;
55 };
56 configData = {
57 "webroot" = {
58 source = pkgs.runCommand "api-webroot" { } ''
59 mkdir -p $out
60 cat > $out/index.html << 'EOF'
61 <!DOCTYPE html>
62 <html>
63 <head><title>API Sub-service</title></head>
64 <body>
65 <h1>API Sub-service</h1>
66 <p>This is a sub-service running on port 8081</p>
67 </body>
68 </html>
69 EOF
70 cat > $out/status.json << 'EOF'
71 {"status": "ok", "service": "api", "port": 8081}
72 EOF
73 '';
74 };
75 };
76 };
77 };
78
79 networking.firewall.allowedTCPPorts = [
80 8080
81 8081
82 ];
83
84 specialisation.updated.configuration = {
85 system.services.webserver = {
86 configData = {
87 "webroot" = {
88 source = lib.mkForce (
89 pkgs.runCommand "webroot-updated" { } ''
90 mkdir -p $out
91 cat > $out/index.html << 'EOF'
92 <!DOCTYPE html>
93 <html>
94 <head><title>Updated Python Web Server</title></head>
95 <body>
96 <h1>Updated content via specialisation</h1>
97 <p>This content was changed without restarting the service</p>
98 </body>
99 </html>
100 EOF
101 ''
102 );
103 };
104 };
105
106 services.api = {
107 configData = {
108 "webroot" = {
109 source = lib.mkForce (
110 pkgs.runCommand "api-webroot-updated" { } ''
111 mkdir -p $out
112 cat > $out/index.html << 'EOF'
113 <!DOCTYPE html>
114 <html>
115 <head><title>Updated API Sub-service</title></head>
116 <body>
117 <h1>Updated API Sub-service</h1>
118 <p>This sub-service content was also updated</p>
119 </body>
120 </html>
121 EOF
122 cat > $out/status.json << 'EOF'
123 {"status": "updated", "service": "api", "port": 8081, "version": "2.0"}
124 EOF
125 ''
126 );
127 };
128 };
129 };
130 };
131 };
132 };
133
134 client =
135 { pkgs, ... }:
136 {
137 environment.systemPackages = [ pkgs.curl ];
138 };
139 };
140
141 testScript = ''
142 start_all()
143
144 server.wait_for_unit("multi-user.target")
145 client.wait_for_unit("multi-user.target")
146
147 # Wait for the web servers to start
148 server.wait_for_unit("webserver.service")
149 server.wait_for_open_port(8080)
150 server.wait_for_unit("webserver-api.service")
151 server.wait_for_open_port(8081)
152
153 # Check that the configData directories were created with unique paths
154 server.succeed("test -d /etc/system-services/webserver/webroot")
155 server.succeed("test -f /etc/system-services/webserver/webroot/index.html")
156 server.succeed("test -d /etc/system-services/webserver-api/webroot")
157 server.succeed("test -f /etc/system-services/webserver-api/webroot/index.html")
158 server.succeed("test -f /etc/system-services/webserver-api/webroot/status.json")
159
160 # Check that the main web server is serving the configData content
161 client.succeed("curl -f http://server:8080/index.html | grep 'Welcome to the Python Web Server'")
162 client.succeed("curl -f http://server:8080/index.html | grep 'Serving from port 8080'")
163
164 # Check that the sub-service is serving its own configData content
165 client.succeed("curl -f http://server:8081/index.html | grep 'API Sub-service'")
166 client.succeed("curl -f http://server:8081/index.html | grep 'This is a sub-service running on port 8081'")
167 client.succeed("curl -f http://server:8081/status.json | grep '\"service\": \"api\"'")
168
169 # Record PIDs before switching to verify services aren't restarted
170 webserver_pid = server.succeed("systemctl show webserver.service --property=MainPID --value").strip()
171 api_pid = server.succeed("systemctl show webserver-api.service --property=MainPID --value").strip()
172
173 print(f"Before switch - webserver PID: {webserver_pid}, api PID: {api_pid}")
174
175 # Switch to the specialisation with updated content
176 # Capture both stdout and stderr, and show stderr in real-time for debugging
177 switch_output = server.succeed("/run/current-system/specialisation/updated/bin/switch-to-configuration test 2>&1 | tee /dev/stderr")
178 print(f"Switch output (stdout+stderr): {switch_output}")
179
180 # Verify services are not mentioned in the switch output (indicating they weren't touched)
181 assert "webserver.service" not in switch_output, f"webserver.service was mentioned in switch output: {switch_output}"
182 assert "webserver-api.service" not in switch_output, f"webserver-api.service was mentioned in switch output: {switch_output}"
183
184 # Verify the content was updated without restarting the services
185 server.succeed("systemctl is-active webserver.service")
186 server.succeed("systemctl is-active webserver-api.service")
187
188 # Verify PIDs are the same (services weren't restarted)
189 webserver_pid_after = server.succeed("systemctl show webserver.service --property=MainPID --value").strip()
190 api_pid_after = server.succeed("systemctl show webserver-api.service --property=MainPID --value").strip()
191
192 print(f"After switch - webserver PID: {webserver_pid_after}, api PID: {api_pid_after}")
193
194 assert webserver_pid == webserver_pid_after, f"webserver.service was restarted: PID changed from {webserver_pid} to {webserver_pid_after}"
195 assert api_pid == api_pid_after, f"webserver-api.service was restarted: PID changed from {api_pid} to {api_pid_after}"
196
197 # Check main service updated content
198 client.succeed("curl -f http://server:8080/index.html | grep 'Updated content via specialisation'")
199 client.succeed("curl -f http://server:8080/index.html | grep 'This content was changed without restarting the service'")
200
201 # Check sub-service updated content
202 client.succeed("curl -f http://server:8081/index.html | grep 'Updated API Sub-service'")
203 client.succeed("curl -f http://server:8081/index.html | grep 'This sub-service content was also updated'")
204 client.succeed("curl -f http://server:8081/status.json | grep '\"version\": \"2.0\"'")
205 '';
206
207 meta.maintainers = with lib.maintainers; [ roberth ];
208}