tangled
alpha
login
or
join now
pyrox.dev
/
nixpkgs
lol
0
fork
atom
overview
issues
pulls
pipelines
nixos/modules: add nominatim module and test
Ivan Mincik
7 months ago
5cd09e28
57174412
+513
4 changed files
expand all
collapse all
unified
split
nixos
modules
module-list.nix
services
search
nominatim.nix
tests
all-tests.nix
nominatim.nix
+1
nixos/modules/module-list.nix
···
1413
1413
./services/search/hound.nix
1414
1414
./services/search/manticore.nix
1415
1415
./services/search/meilisearch.nix
1416
1416
+
./services/search/nominatim.nix
1416
1417
./services/search/opensearch.nix
1417
1418
./services/search/qdrant.nix
1418
1419
./services/search/quickwit.nix
+324
nixos/modules/services/search/nominatim.nix
···
1
1
+
{
2
2
+
lib,
3
3
+
config,
4
4
+
pkgs,
5
5
+
...
6
6
+
}:
7
7
+
8
8
+
let
9
9
+
cfg = config.services.nominatim;
10
10
+
11
11
+
localDb = cfg.database.host == "localhost";
12
12
+
uiPackage = cfg.ui.package.override { customConfig = cfg.ui.config; };
13
13
+
in
14
14
+
{
15
15
+
options.services.nominatim = {
16
16
+
enable = lib.mkOption {
17
17
+
type = lib.types.bool;
18
18
+
default = false;
19
19
+
description = ''
20
20
+
Whether to enable nominatim.
21
21
+
22
22
+
Also enables nginx virtual host management. Further nginx configuration
23
23
+
can be done by adapting `services.nginx.virtualHosts.<name>`.
24
24
+
See [](#opt-services.nginx.virtualHosts).
25
25
+
'';
26
26
+
};
27
27
+
28
28
+
package = lib.mkPackageOption pkgs.python3Packages "nominatim-api" { };
29
29
+
30
30
+
hostName = lib.mkOption {
31
31
+
type = lib.types.str;
32
32
+
description = "Hostname to use for the nginx vhost.";
33
33
+
example = "nominatim.example.com";
34
34
+
};
35
35
+
36
36
+
settings = lib.mkOption {
37
37
+
default = { };
38
38
+
type = lib.types.attrsOf lib.types.str;
39
39
+
example = lib.literalExpression ''
40
40
+
{
41
41
+
NOMINATIM_REPLICATION_URL = "https://planet.openstreetmap.org/replication/minute";
42
42
+
NOMINATIM_REPLICATION_MAX_DIFF = "100";
43
43
+
}
44
44
+
'';
45
45
+
description = ''
46
46
+
Nominatim configuration settings.
47
47
+
For the list of available configuration options see
48
48
+
<https://nominatim.org/release-docs/latest/customize/Settings>.
49
49
+
'';
50
50
+
};
51
51
+
52
52
+
ui = {
53
53
+
package = lib.mkPackageOption pkgs "nominatim-ui" { };
54
54
+
55
55
+
config = lib.mkOption {
56
56
+
type = lib.types.nullOr lib.types.str;
57
57
+
default = null;
58
58
+
description = ''
59
59
+
Nominatim UI configuration placed to theme/config.theme.js file.
60
60
+
61
61
+
For the list of available configuration options see
62
62
+
<https://github.com/osm-search/nominatim-ui/blob/master/dist/config.defaults.js>.
63
63
+
'';
64
64
+
example = ''
65
65
+
Nominatim_Config.Page_Title='My Nominatim instance';
66
66
+
Nominatim_Config.Nominatim_API_Endpoint='https://localhost/';
67
67
+
'';
68
68
+
};
69
69
+
};
70
70
+
71
71
+
database = {
72
72
+
host = lib.mkOption {
73
73
+
type = lib.types.str;
74
74
+
default = "localhost";
75
75
+
description = ''
76
76
+
Host of the postgresql server. If not set to `localhost`, Nominatim
77
77
+
database and postgresql superuser with appropriate permissions must
78
78
+
exist on target host.
79
79
+
'';
80
80
+
};
81
81
+
82
82
+
port = lib.mkOption {
83
83
+
type = lib.types.port;
84
84
+
default = 5432;
85
85
+
description = "Port of the postgresql database.";
86
86
+
};
87
87
+
88
88
+
dbname = lib.mkOption {
89
89
+
type = lib.types.str;
90
90
+
default = "nominatim";
91
91
+
description = "Name of the postgresql database.";
92
92
+
};
93
93
+
94
94
+
superUser = lib.mkOption {
95
95
+
type = lib.types.str;
96
96
+
default = "nominatim";
97
97
+
description = ''
98
98
+
Postgresql database superuser used to create Nominatim database and
99
99
+
import data. If `database.host` is set to `localhost`, a unix user and
100
100
+
group of the same name will be automatically created.
101
101
+
'';
102
102
+
};
103
103
+
104
104
+
apiUser = lib.mkOption {
105
105
+
type = lib.types.str;
106
106
+
default = "nominatim-api";
107
107
+
description = ''
108
108
+
Postgresql database user with read-only permissions used for Nominatim
109
109
+
web API service.
110
110
+
'';
111
111
+
};
112
112
+
113
113
+
passwordFile = lib.mkOption {
114
114
+
type = lib.types.nullOr lib.types.path;
115
115
+
default = null;
116
116
+
description = ''
117
117
+
Password file used for Nominatim database connection.
118
118
+
Must be readable only for the Nominatim web API user.
119
119
+
120
120
+
The file must be a valid `.pgpass` file as described in:
121
121
+
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
122
122
+
123
123
+
In most cases, the following will be enough:
124
124
+
```
125
125
+
*:*:*:*:<password>
126
126
+
```
127
127
+
'';
128
128
+
};
129
129
+
130
130
+
extraConnectionParams = lib.mkOption {
131
131
+
type = lib.types.nullOr lib.types.str;
132
132
+
default = null;
133
133
+
description = ''
134
134
+
Extra Nominatim database connection parameters.
135
135
+
136
136
+
Format:
137
137
+
<param1>=<value1>;<param2>=<value2>
138
138
+
139
139
+
See <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>.
140
140
+
'';
141
141
+
};
142
142
+
};
143
143
+
};
144
144
+
145
145
+
config =
146
146
+
let
147
147
+
nominatimSuperUserDsn =
148
148
+
"pgsql:dbname=${cfg.database.dbname};"
149
149
+
+ "user=${cfg.database.superUser}"
150
150
+
+ lib.optionalString (cfg.database.extraConnectionParams != null) (
151
151
+
";" + cfg.database.extraConnectionParams
152
152
+
);
153
153
+
154
154
+
nominatimApiDsn =
155
155
+
"pgsql:dbname=${cfg.database.dbname}"
156
156
+
+ lib.optionalString (!localDb) (
157
157
+
";host=${cfg.database.host};"
158
158
+
+ "port=${toString cfg.database.port};"
159
159
+
+ "user=${cfg.database.apiUser}"
160
160
+
)
161
161
+
+ lib.optionalString (cfg.database.extraConnectionParams != null) (
162
162
+
";" + cfg.database.extraConnectionParams
163
163
+
);
164
164
+
in
165
165
+
lib.mkIf cfg.enable {
166
166
+
# CLI package
167
167
+
environment.systemPackages = [ pkgs.nominatim ];
168
168
+
169
169
+
# Database
170
170
+
users.users.${cfg.database.superUser} = lib.mkIf localDb {
171
171
+
group = cfg.database.superUser;
172
172
+
isSystemUser = true;
173
173
+
createHome = false;
174
174
+
};
175
175
+
users.groups.${cfg.database.superUser} = lib.mkIf localDb { };
176
176
+
177
177
+
services.postgresql = lib.mkIf localDb {
178
178
+
enable = true;
179
179
+
extensions = ps: with ps; [ postgis ];
180
180
+
ensureUsers = [
181
181
+
{
182
182
+
name = cfg.database.superUser;
183
183
+
ensureClauses.superuser = true;
184
184
+
}
185
185
+
{
186
186
+
name = cfg.database.apiUser;
187
187
+
}
188
188
+
];
189
189
+
};
190
190
+
191
191
+
# TODO: add nominatim-update service
192
192
+
193
193
+
systemd.services.nominatim-init = lib.mkIf localDb {
194
194
+
after = [ "postgresql-setup.service" ];
195
195
+
requires = [ "postgresql-setup.service" ];
196
196
+
wantedBy = [ "multi-user.target" ];
197
197
+
serviceConfig = {
198
198
+
Type = "oneshot";
199
199
+
User = cfg.database.superUser;
200
200
+
RemainAfterExit = true;
201
201
+
PrivateTmp = true;
202
202
+
};
203
203
+
script = ''
204
204
+
sql="SELECT COUNT(*) FROM pg_database WHERE datname='${cfg.database.dbname}'"
205
205
+
db_exists=$(${pkgs.postgresql}/bin/psql --dbname postgres -tAc "$sql")
206
206
+
207
207
+
if [ "$db_exists" == "0" ]; then
208
208
+
${lib.getExe pkgs.nominatim} import --prepare-database
209
209
+
else
210
210
+
echo "Database ${cfg.database.dbname} already exists. Skipping ..."
211
211
+
fi
212
212
+
'';
213
213
+
path = [
214
214
+
pkgs.postgresql
215
215
+
];
216
216
+
environment = {
217
217
+
NOMINATIM_DATABASE_DSN = nominatimSuperUserDsn;
218
218
+
NOMINATIM_DATABASE_WEBUSER = cfg.database.apiUser;
219
219
+
} // cfg.settings;
220
220
+
};
221
221
+
222
222
+
# Web API service
223
223
+
users.users.${cfg.database.apiUser} = {
224
224
+
group = cfg.database.apiUser;
225
225
+
isSystemUser = true;
226
226
+
createHome = false;
227
227
+
};
228
228
+
users.groups.${cfg.database.apiUser} = { };
229
229
+
230
230
+
systemd.services.nominatim = {
231
231
+
after = [ "network.target" ] ++ lib.optionals localDb [ "nominatim-init.service" ];
232
232
+
requires = lib.optionals localDb [ "nominatim-init.service" ];
233
233
+
bindsTo = lib.optionals localDb [ "postgresql.service" ];
234
234
+
wantedBy = [ "multi-user.target" ];
235
235
+
wants = [ "network.target" ];
236
236
+
serviceConfig = {
237
237
+
Type = "simple";
238
238
+
User = cfg.database.apiUser;
239
239
+
ExecStart = ''
240
240
+
${pkgs.python3Packages.gunicorn}/bin/gunicorn \
241
241
+
--bind unix:/run/nominatim.sock \
242
242
+
--workers 4 \
243
243
+
--worker-class uvicorn.workers.UvicornWorker "nominatim_api.server.falcon.server:run_wsgi()"
244
244
+
'';
245
245
+
Environment = lib.optional (
246
246
+
cfg.database.passwordFile != null
247
247
+
) "PGPASSFILE=${cfg.database.passwordFile}";
248
248
+
ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
249
249
+
KillMode = "mixed";
250
250
+
TimeoutStopSec = 5;
251
251
+
};
252
252
+
environment = {
253
253
+
PYTHONPATH =
254
254
+
with pkgs.python3Packages;
255
255
+
pkgs.python3Packages.makePythonPath [
256
256
+
cfg.package
257
257
+
falcon
258
258
+
uvicorn
259
259
+
];
260
260
+
NOMINATIM_DATABASE_DSN = nominatimApiDsn;
261
261
+
NOMINATIM_DATABASE_WEBUSER = cfg.database.apiUser;
262
262
+
} // cfg.settings;
263
263
+
};
264
264
+
265
265
+
systemd.sockets.nominatim = {
266
266
+
before = [ "nominatim.service" ];
267
267
+
wantedBy = [ "sockets.target" ];
268
268
+
socketConfig = {
269
269
+
ListenStream = "/run/nominatim.sock";
270
270
+
SocketUser = cfg.database.apiUser;
271
271
+
SocketGroup = config.services.nginx.group;
272
272
+
};
273
273
+
};
274
274
+
275
275
+
services.nginx = {
276
276
+
enable = true;
277
277
+
appendHttpConfig = ''
278
278
+
map $args $format {
279
279
+
default default;
280
280
+
~(^|&)format=html(&|$) html;
281
281
+
}
282
282
+
283
283
+
map $uri/$format $forward_to_ui {
284
284
+
default 0; # No forwarding by default.
285
285
+
286
286
+
# Redirect to HTML UI if explicitly requested.
287
287
+
~/reverse.*/html 1;
288
288
+
~/search.*/html 1;
289
289
+
~/lookup.*/html 1;
290
290
+
~/details.*/html 1;
291
291
+
}
292
292
+
'';
293
293
+
upstreams.nominatim = {
294
294
+
servers = {
295
295
+
"unix:/run/nominatim.sock" = { };
296
296
+
};
297
297
+
};
298
298
+
virtualHosts = {
299
299
+
${cfg.hostName} = {
300
300
+
forceSSL = lib.mkDefault true;
301
301
+
enableACME = lib.mkDefault true;
302
302
+
locations = {
303
303
+
"= /" = {
304
304
+
extraConfig = ''
305
305
+
return 301 $scheme://$http_host/ui/search.html;
306
306
+
'';
307
307
+
};
308
308
+
"/" = {
309
309
+
proxyPass = "http://nominatim";
310
310
+
extraConfig = ''
311
311
+
if ($forward_to_ui) {
312
312
+
rewrite ^(/[^/.]*) /ui$1.html redirect;
313
313
+
}
314
314
+
'';
315
315
+
};
316
316
+
"/ui/" = {
317
317
+
alias = "${uiPackage}/";
318
318
+
};
319
319
+
};
320
320
+
};
321
321
+
};
322
322
+
};
323
323
+
};
324
324
+
}
+1
nixos/tests/all-tests.nix
···
1012
1012
nixseparatedebuginfod = runTest ./nixseparatedebuginfod.nix;
1013
1013
node-red = runTest ./node-red.nix;
1014
1014
nomad = runTest ./nomad.nix;
1015
1015
+
nominatim = runTest ./nominatim.nix;
1015
1016
non-default-filesystems = handleTest ./non-default-filesystems.nix { };
1016
1017
non-switchable-system = runTest ./non-switchable-system.nix;
1017
1018
noto-fonts = runTest ./noto-fonts.nix;
+187
nixos/tests/nominatim.nix
···
1
1
+
{ pkgs, lib, ... }:
2
2
+
3
3
+
let
4
4
+
# Andorra - the smallest dataset in Europe (3.1 MB)
5
5
+
osmData = pkgs.fetchurl {
6
6
+
url = "https://web.archive.org/web/20250430211212/https://download.geofabrik.de/europe/andorra-latest.osm.pbf";
7
7
+
hash = "sha256-Ey+ipTOFUm80rxBteirPW5N4KxmUsg/pCE58E/2rcyE=";
8
8
+
};
9
9
+
in
10
10
+
{
11
11
+
name = "nominatim";
12
12
+
meta = {
13
13
+
maintainers = with lib.teams; [
14
14
+
geospatial
15
15
+
ngi
16
16
+
];
17
17
+
};
18
18
+
19
19
+
nodes = {
20
20
+
# nominatim - self contained host
21
21
+
nominatim =
22
22
+
{ config, pkgs, ... }:
23
23
+
{
24
24
+
# Nominatim
25
25
+
services.nominatim = {
26
26
+
enable = true;
27
27
+
hostName = "nominatim";
28
28
+
settings = {
29
29
+
NOMINATIM_IMPORT_STYLE = "admin";
30
30
+
};
31
31
+
ui = {
32
32
+
config = ''
33
33
+
Nominatim_Config.Page_Title='Test Nominatim instance';
34
34
+
Nominatim_Config.Nominatim_API_Endpoint='https://localhost/';
35
35
+
'';
36
36
+
};
37
37
+
};
38
38
+
39
39
+
# Disable SSL
40
40
+
services.nginx.virtualHosts.nominatim = {
41
41
+
forceSSL = false;
42
42
+
enableACME = false;
43
43
+
};
44
44
+
45
45
+
# Database
46
46
+
services.postgresql = {
47
47
+
enableTCPIP = true;
48
48
+
authentication = lib.mkForce ''
49
49
+
local all all trust
50
50
+
host all all 0.0.0.0/0 md5
51
51
+
host all all ::0/0 md5
52
52
+
'';
53
53
+
};
54
54
+
systemd.services.postgresql-setup.postStart = ''
55
55
+
psql --command "ALTER ROLE \"nominatim-api\" WITH PASSWORD 'password';"
56
56
+
'';
57
57
+
networking.firewall.allowedTCPPorts = [ config.services.postgresql.settings.port ];
58
58
+
};
59
59
+
60
60
+
# api - web API only
61
61
+
api =
62
62
+
{ config, pkgs, ... }:
63
63
+
{
64
64
+
# Database password
65
65
+
system.activationScripts = {
66
66
+
passwordFile.text = with config.services.nominatim.database; ''
67
67
+
mkdir -p /run/secrets
68
68
+
echo "${host}:${toString port}:${dbname}:${apiUser}:password" \
69
69
+
> /run/secrets/pgpass
70
70
+
chown nominatim-api:nominatim-api /run/secrets/pgpass
71
71
+
chmod 0600 /run/secrets/pgpass
72
72
+
'';
73
73
+
};
74
74
+
75
75
+
# Nominatim
76
76
+
services.nominatim = {
77
77
+
enable = true;
78
78
+
hostName = "nominatim";
79
79
+
settings = {
80
80
+
NOMINATIM_LOG_DB = "yes";
81
81
+
};
82
82
+
database = {
83
83
+
host = "nominatim";
84
84
+
passwordFile = "/run/secrets/pgpass";
85
85
+
extraConnectionParams = "application_name=nominatim;connect_timeout=2";
86
86
+
};
87
87
+
};
88
88
+
89
89
+
# Disable SSL
90
90
+
services.nginx.virtualHosts.nominatim = {
91
91
+
forceSSL = false;
92
92
+
enableACME = false;
93
93
+
};
94
94
+
};
95
95
+
};
96
96
+
97
97
+
testScript = ''
98
98
+
# Test nominatim host
99
99
+
nominatim.start()
100
100
+
nominatim.wait_for_unit("nominatim.service")
101
101
+
102
102
+
# Import OSM data
103
103
+
nominatim.succeed("""
104
104
+
cd /tmp
105
105
+
sudo -u nominatim \
106
106
+
NOMINATIM_DATABASE_WEBUSER=nominatim-api \
107
107
+
NOMINATIM_IMPORT_STYLE=admin \
108
108
+
nominatim import --continue import-from-file --osm-file ${osmData}
109
109
+
""")
110
110
+
nominatim.succeed("systemctl restart nominatim.service")
111
111
+
112
112
+
# Test CLI
113
113
+
nominatim.succeed("sudo -u nominatim-api nominatim search --query Andorra")
114
114
+
115
115
+
# Test web API
116
116
+
nominatim.succeed("curl 'http://localhost/status' | grep OK")
117
117
+
118
118
+
nominatim.succeed("""
119
119
+
curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
120
120
+
curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
121
121
+
""")
122
122
+
123
123
+
# Test UI
124
124
+
nominatim.succeed("""
125
125
+
curl "http://localhost/ui/search.html" \
126
126
+
| grep "<title>Nominatim Demo</title>"
127
127
+
""")
128
128
+
129
129
+
130
130
+
# Test api host
131
131
+
api.start()
132
132
+
api.wait_for_unit("nominatim.service")
133
133
+
134
134
+
# Test web API
135
135
+
api.succeed("""
136
136
+
curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
137
137
+
curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
138
138
+
""")
139
139
+
140
140
+
141
141
+
# Test format rewrites
142
142
+
# Redirect / to search
143
143
+
nominatim.succeed("""
144
144
+
curl --verbose "http://localhost" 2>&1 \
145
145
+
| grep "Location: http://localhost/ui/search.html"
146
146
+
""")
147
147
+
148
148
+
# Return text by default
149
149
+
nominatim.succeed("""
150
150
+
curl --verbose "http://localhost/status" 2>&1 \
151
151
+
| grep "Content-Type: text/plain"
152
152
+
""")
153
153
+
154
154
+
# Return JSON by default
155
155
+
nominatim.succeed("""
156
156
+
curl --verbose "http://localhost/search?q=Andorra" 2>&1 \
157
157
+
| grep "Content-Type: application/json"
158
158
+
""")
159
159
+
160
160
+
# Return XML by default
161
161
+
nominatim.succeed("""
162
162
+
curl --verbose "http://localhost/lookup" 2>&1 \
163
163
+
| grep "Content-Type: text/xml"
164
164
+
165
165
+
curl --verbose "http://localhost/reverse?lat=0&lon=0" 2>&1 \
166
166
+
| grep "Content-Type: text/xml"
167
167
+
""")
168
168
+
169
169
+
# Redirect explicitly requested HTML format
170
170
+
nominatim.succeed("""
171
171
+
curl --verbose "http://localhost/search?format=html" 2>&1 \
172
172
+
| grep "Location: http://localhost/ui/search.html"
173
173
+
174
174
+
curl --verbose "http://localhost/reverse?format=html" 2>&1 \
175
175
+
| grep "Location: http://localhost/ui/reverse.html"
176
176
+
""")
177
177
+
178
178
+
# Return explicitly requested JSON format
179
179
+
nominatim.succeed("""
180
180
+
curl --verbose "http://localhost/search?format=json" 2>&1 \
181
181
+
| grep "Content-Type: application/json"
182
182
+
183
183
+
curl --verbose "http://localhost/reverse?format=json" 2>&1 \
184
184
+
| grep "Content-Type: application/json"
185
185
+
""")
186
186
+
'';
187
187
+
}