nixos/modules: add nominatim module and test (#420050)
This commit is contained in:
commit
6d97fbb83e
@ -1413,6 +1413,7 @@
|
||||
./services/search/hound.nix
|
||||
./services/search/manticore.nix
|
||||
./services/search/meilisearch.nix
|
||||
./services/search/nominatim.nix
|
||||
./services/search/opensearch.nix
|
||||
./services/search/qdrant.nix
|
||||
./services/search/quickwit.nix
|
||||
|
||||
324
nixos/modules/services/search/nominatim.nix
Normal file
324
nixos/modules/services/search/nominatim.nix
Normal file
@ -0,0 +1,324 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.services.nominatim;
|
||||
|
||||
localDb = cfg.database.host == "localhost";
|
||||
uiPackage = cfg.ui.package.override { customConfig = cfg.ui.config; };
|
||||
in
|
||||
{
|
||||
options.services.nominatim = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable nominatim.
|
||||
|
||||
Also enables nginx virtual host management. Further nginx configuration
|
||||
can be done by adapting `services.nginx.virtualHosts.<name>`.
|
||||
See [](#opt-services.nginx.virtualHosts).
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkPackageOption pkgs.python3Packages "nominatim-api" { };
|
||||
|
||||
hostName = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Hostname to use for the nginx vhost.";
|
||||
example = "nominatim.example.com";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
default = { };
|
||||
type = lib.types.attrsOf lib.types.str;
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
NOMINATIM_REPLICATION_URL = "https://planet.openstreetmap.org/replication/minute";
|
||||
NOMINATIM_REPLICATION_MAX_DIFF = "100";
|
||||
}
|
||||
'';
|
||||
description = ''
|
||||
Nominatim configuration settings.
|
||||
For the list of available configuration options see
|
||||
<https://nominatim.org/release-docs/latest/customize/Settings>.
|
||||
'';
|
||||
};
|
||||
|
||||
ui = {
|
||||
package = lib.mkPackageOption pkgs "nominatim-ui" { };
|
||||
|
||||
config = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Nominatim UI configuration placed to theme/config.theme.js file.
|
||||
|
||||
For the list of available configuration options see
|
||||
<https://github.com/osm-search/nominatim-ui/blob/master/dist/config.defaults.js>.
|
||||
'';
|
||||
example = ''
|
||||
Nominatim_Config.Page_Title='My Nominatim instance';
|
||||
Nominatim_Config.Nominatim_API_Endpoint='https://localhost/';
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
database = {
|
||||
host = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Host of the postgresql server. If not set to `localhost`, Nominatim
|
||||
database and postgresql superuser with appropriate permissions must
|
||||
exist on target host.
|
||||
'';
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = lib.types.port;
|
||||
default = 5432;
|
||||
description = "Port of the postgresql database.";
|
||||
};
|
||||
|
||||
dbname = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nominatim";
|
||||
description = "Name of the postgresql database.";
|
||||
};
|
||||
|
||||
superUser = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nominatim";
|
||||
description = ''
|
||||
Postgresql database superuser used to create Nominatim database and
|
||||
import data. If `database.host` is set to `localhost`, a unix user and
|
||||
group of the same name will be automatically created.
|
||||
'';
|
||||
};
|
||||
|
||||
apiUser = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "nominatim-api";
|
||||
description = ''
|
||||
Postgresql database user with read-only permissions used for Nominatim
|
||||
web API service.
|
||||
'';
|
||||
};
|
||||
|
||||
passwordFile = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Password file used for Nominatim database connection.
|
||||
Must be readable only for the Nominatim web API user.
|
||||
|
||||
The file must be a valid `.pgpass` file as described in:
|
||||
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
|
||||
|
||||
In most cases, the following will be enough:
|
||||
```
|
||||
*:*:*:*:<password>
|
||||
```
|
||||
'';
|
||||
};
|
||||
|
||||
extraConnectionParams = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Extra Nominatim database connection parameters.
|
||||
|
||||
Format:
|
||||
<param1>=<value1>;<param2>=<value2>
|
||||
|
||||
See <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
nominatimSuperUserDsn =
|
||||
"pgsql:dbname=${cfg.database.dbname};"
|
||||
+ "user=${cfg.database.superUser}"
|
||||
+ lib.optionalString (cfg.database.extraConnectionParams != null) (
|
||||
";" + cfg.database.extraConnectionParams
|
||||
);
|
||||
|
||||
nominatimApiDsn =
|
||||
"pgsql:dbname=${cfg.database.dbname}"
|
||||
+ lib.optionalString (!localDb) (
|
||||
";host=${cfg.database.host};"
|
||||
+ "port=${toString cfg.database.port};"
|
||||
+ "user=${cfg.database.apiUser}"
|
||||
)
|
||||
+ lib.optionalString (cfg.database.extraConnectionParams != null) (
|
||||
";" + cfg.database.extraConnectionParams
|
||||
);
|
||||
in
|
||||
lib.mkIf cfg.enable {
|
||||
# CLI package
|
||||
environment.systemPackages = [ pkgs.nominatim ];
|
||||
|
||||
# Database
|
||||
users.users.${cfg.database.superUser} = lib.mkIf localDb {
|
||||
group = cfg.database.superUser;
|
||||
isSystemUser = true;
|
||||
createHome = false;
|
||||
};
|
||||
users.groups.${cfg.database.superUser} = lib.mkIf localDb { };
|
||||
|
||||
services.postgresql = lib.mkIf localDb {
|
||||
enable = true;
|
||||
extensions = ps: with ps; [ postgis ];
|
||||
ensureUsers = [
|
||||
{
|
||||
name = cfg.database.superUser;
|
||||
ensureClauses.superuser = true;
|
||||
}
|
||||
{
|
||||
name = cfg.database.apiUser;
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# TODO: add nominatim-update service
|
||||
|
||||
systemd.services.nominatim-init = lib.mkIf localDb {
|
||||
after = [ "postgresql-setup.service" ];
|
||||
requires = [ "postgresql-setup.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = cfg.database.superUser;
|
||||
RemainAfterExit = true;
|
||||
PrivateTmp = true;
|
||||
};
|
||||
script = ''
|
||||
sql="SELECT COUNT(*) FROM pg_database WHERE datname='${cfg.database.dbname}'"
|
||||
db_exists=$(${pkgs.postgresql}/bin/psql --dbname postgres -tAc "$sql")
|
||||
|
||||
if [ "$db_exists" == "0" ]; then
|
||||
${lib.getExe pkgs.nominatim} import --prepare-database
|
||||
else
|
||||
echo "Database ${cfg.database.dbname} already exists. Skipping ..."
|
||||
fi
|
||||
'';
|
||||
path = [
|
||||
pkgs.postgresql
|
||||
];
|
||||
environment = {
|
||||
NOMINATIM_DATABASE_DSN = nominatimSuperUserDsn;
|
||||
NOMINATIM_DATABASE_WEBUSER = cfg.database.apiUser;
|
||||
} // cfg.settings;
|
||||
};
|
||||
|
||||
# Web API service
|
||||
users.users.${cfg.database.apiUser} = {
|
||||
group = cfg.database.apiUser;
|
||||
isSystemUser = true;
|
||||
createHome = false;
|
||||
};
|
||||
users.groups.${cfg.database.apiUser} = { };
|
||||
|
||||
systemd.services.nominatim = {
|
||||
after = [ "network.target" ] ++ lib.optionals localDb [ "nominatim-init.service" ];
|
||||
requires = lib.optionals localDb [ "nominatim-init.service" ];
|
||||
bindsTo = lib.optionals localDb [ "postgresql.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "network.target" ];
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.database.apiUser;
|
||||
ExecStart = ''
|
||||
${pkgs.python3Packages.gunicorn}/bin/gunicorn \
|
||||
--bind unix:/run/nominatim.sock \
|
||||
--workers 4 \
|
||||
--worker-class uvicorn.workers.UvicornWorker "nominatim_api.server.falcon.server:run_wsgi()"
|
||||
'';
|
||||
Environment = lib.optional (
|
||||
cfg.database.passwordFile != null
|
||||
) "PGPASSFILE=${cfg.database.passwordFile}";
|
||||
ExecReload = "${pkgs.procps}/bin/kill -s HUP $MAINPID";
|
||||
KillMode = "mixed";
|
||||
TimeoutStopSec = 5;
|
||||
};
|
||||
environment = {
|
||||
PYTHONPATH =
|
||||
with pkgs.python3Packages;
|
||||
pkgs.python3Packages.makePythonPath [
|
||||
cfg.package
|
||||
falcon
|
||||
uvicorn
|
||||
];
|
||||
NOMINATIM_DATABASE_DSN = nominatimApiDsn;
|
||||
NOMINATIM_DATABASE_WEBUSER = cfg.database.apiUser;
|
||||
} // cfg.settings;
|
||||
};
|
||||
|
||||
systemd.sockets.nominatim = {
|
||||
before = [ "nominatim.service" ];
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig = {
|
||||
ListenStream = "/run/nominatim.sock";
|
||||
SocketUser = cfg.database.apiUser;
|
||||
SocketGroup = config.services.nginx.group;
|
||||
};
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
appendHttpConfig = ''
|
||||
map $args $format {
|
||||
default default;
|
||||
~(^|&)format=html(&|$) html;
|
||||
}
|
||||
|
||||
map $uri/$format $forward_to_ui {
|
||||
default 0; # No forwarding by default.
|
||||
|
||||
# Redirect to HTML UI if explicitly requested.
|
||||
~/reverse.*/html 1;
|
||||
~/search.*/html 1;
|
||||
~/lookup.*/html 1;
|
||||
~/details.*/html 1;
|
||||
}
|
||||
'';
|
||||
upstreams.nominatim = {
|
||||
servers = {
|
||||
"unix:/run/nominatim.sock" = { };
|
||||
};
|
||||
};
|
||||
virtualHosts = {
|
||||
${cfg.hostName} = {
|
||||
forceSSL = lib.mkDefault true;
|
||||
enableACME = lib.mkDefault true;
|
||||
locations = {
|
||||
"= /" = {
|
||||
extraConfig = ''
|
||||
return 301 $scheme://$http_host/ui/search.html;
|
||||
'';
|
||||
};
|
||||
"/" = {
|
||||
proxyPass = "http://nominatim";
|
||||
extraConfig = ''
|
||||
if ($forward_to_ui) {
|
||||
rewrite ^(/[^/.]*) /ui$1.html redirect;
|
||||
}
|
||||
'';
|
||||
};
|
||||
"/ui/" = {
|
||||
alias = "${uiPackage}/";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -1014,6 +1014,7 @@ in
|
||||
nixseparatedebuginfod = runTest ./nixseparatedebuginfod.nix;
|
||||
node-red = runTest ./node-red.nix;
|
||||
nomad = runTest ./nomad.nix;
|
||||
nominatim = runTest ./nominatim.nix;
|
||||
non-default-filesystems = handleTest ./non-default-filesystems.nix { };
|
||||
non-switchable-system = runTest ./non-switchable-system.nix;
|
||||
noto-fonts = runTest ./noto-fonts.nix;
|
||||
|
||||
187
nixos/tests/nominatim.nix
Normal file
187
nixos/tests/nominatim.nix
Normal file
@ -0,0 +1,187 @@
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
# Andorra - the smallest dataset in Europe (3.1 MB)
|
||||
osmData = pkgs.fetchurl {
|
||||
url = "https://web.archive.org/web/20250430211212/https://download.geofabrik.de/europe/andorra-latest.osm.pbf";
|
||||
hash = "sha256-Ey+ipTOFUm80rxBteirPW5N4KxmUsg/pCE58E/2rcyE=";
|
||||
};
|
||||
in
|
||||
{
|
||||
name = "nominatim";
|
||||
meta = {
|
||||
maintainers = with lib.teams; [
|
||||
geospatial
|
||||
ngi
|
||||
];
|
||||
};
|
||||
|
||||
nodes = {
|
||||
# nominatim - self contained host
|
||||
nominatim =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# Nominatim
|
||||
services.nominatim = {
|
||||
enable = true;
|
||||
hostName = "nominatim";
|
||||
settings = {
|
||||
NOMINATIM_IMPORT_STYLE = "admin";
|
||||
};
|
||||
ui = {
|
||||
config = ''
|
||||
Nominatim_Config.Page_Title='Test Nominatim instance';
|
||||
Nominatim_Config.Nominatim_API_Endpoint='https://localhost/';
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# Disable SSL
|
||||
services.nginx.virtualHosts.nominatim = {
|
||||
forceSSL = false;
|
||||
enableACME = false;
|
||||
};
|
||||
|
||||
# Database
|
||||
services.postgresql = {
|
||||
enableTCPIP = true;
|
||||
authentication = lib.mkForce ''
|
||||
local all all trust
|
||||
host all all 0.0.0.0/0 md5
|
||||
host all all ::0/0 md5
|
||||
'';
|
||||
};
|
||||
systemd.services.postgresql-setup.postStart = ''
|
||||
psql --command "ALTER ROLE \"nominatim-api\" WITH PASSWORD 'password';"
|
||||
'';
|
||||
networking.firewall.allowedTCPPorts = [ config.services.postgresql.settings.port ];
|
||||
};
|
||||
|
||||
# api - web API only
|
||||
api =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
# Database password
|
||||
system.activationScripts = {
|
||||
passwordFile.text = with config.services.nominatim.database; ''
|
||||
mkdir -p /run/secrets
|
||||
echo "${host}:${toString port}:${dbname}:${apiUser}:password" \
|
||||
> /run/secrets/pgpass
|
||||
chown nominatim-api:nominatim-api /run/secrets/pgpass
|
||||
chmod 0600 /run/secrets/pgpass
|
||||
'';
|
||||
};
|
||||
|
||||
# Nominatim
|
||||
services.nominatim = {
|
||||
enable = true;
|
||||
hostName = "nominatim";
|
||||
settings = {
|
||||
NOMINATIM_LOG_DB = "yes";
|
||||
};
|
||||
database = {
|
||||
host = "nominatim";
|
||||
passwordFile = "/run/secrets/pgpass";
|
||||
extraConnectionParams = "application_name=nominatim;connect_timeout=2";
|
||||
};
|
||||
};
|
||||
|
||||
# Disable SSL
|
||||
services.nginx.virtualHosts.nominatim = {
|
||||
forceSSL = false;
|
||||
enableACME = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
# Test nominatim host
|
||||
nominatim.start()
|
||||
nominatim.wait_for_unit("nominatim.service")
|
||||
|
||||
# Import OSM data
|
||||
nominatim.succeed("""
|
||||
cd /tmp
|
||||
sudo -u nominatim \
|
||||
NOMINATIM_DATABASE_WEBUSER=nominatim-api \
|
||||
NOMINATIM_IMPORT_STYLE=admin \
|
||||
nominatim import --continue import-from-file --osm-file ${osmData}
|
||||
""")
|
||||
nominatim.succeed("systemctl restart nominatim.service")
|
||||
|
||||
# Test CLI
|
||||
nominatim.succeed("sudo -u nominatim-api nominatim search --query Andorra")
|
||||
|
||||
# Test web API
|
||||
nominatim.succeed("curl 'http://localhost/status' | grep OK")
|
||||
|
||||
nominatim.succeed("""
|
||||
curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
|
||||
curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
|
||||
""")
|
||||
|
||||
# Test UI
|
||||
nominatim.succeed("""
|
||||
curl "http://localhost/ui/search.html" \
|
||||
| grep "<title>Nominatim Demo</title>"
|
||||
""")
|
||||
|
||||
|
||||
# Test api host
|
||||
api.start()
|
||||
api.wait_for_unit("nominatim.service")
|
||||
|
||||
# Test web API
|
||||
api.succeed("""
|
||||
curl "http://localhost/search?q=Andorra&format=geojson" | grep "Andorra"
|
||||
curl "http://localhost/reverse?lat=42.5407167&lon=1.5732033&format=geojson"
|
||||
""")
|
||||
|
||||
|
||||
# Test format rewrites
|
||||
# Redirect / to search
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost" 2>&1 \
|
||||
| grep "Location: http://localhost/ui/search.html"
|
||||
""")
|
||||
|
||||
# Return text by default
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost/status" 2>&1 \
|
||||
| grep "Content-Type: text/plain"
|
||||
""")
|
||||
|
||||
# Return JSON by default
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost/search?q=Andorra" 2>&1 \
|
||||
| grep "Content-Type: application/json"
|
||||
""")
|
||||
|
||||
# Return XML by default
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost/lookup" 2>&1 \
|
||||
| grep "Content-Type: text/xml"
|
||||
|
||||
curl --verbose "http://localhost/reverse?lat=0&lon=0" 2>&1 \
|
||||
| grep "Content-Type: text/xml"
|
||||
""")
|
||||
|
||||
# Redirect explicitly requested HTML format
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost/search?format=html" 2>&1 \
|
||||
| grep "Location: http://localhost/ui/search.html"
|
||||
|
||||
curl --verbose "http://localhost/reverse?format=html" 2>&1 \
|
||||
| grep "Location: http://localhost/ui/reverse.html"
|
||||
""")
|
||||
|
||||
# Return explicitly requested JSON format
|
||||
nominatim.succeed("""
|
||||
curl --verbose "http://localhost/search?format=json" 2>&1 \
|
||||
| grep "Content-Type: application/json"
|
||||
|
||||
curl --verbose "http://localhost/reverse?format=json" 2>&1 \
|
||||
| grep "Content-Type: application/json"
|
||||
""")
|
||||
'';
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
stdenv,
|
||||
fetchFromGitHub,
|
||||
fetchYarnDeps,
|
||||
nixosTests,
|
||||
writableTmpDirAsHomeHook,
|
||||
writeText,
|
||||
|
||||
@ -10,7 +11,7 @@
|
||||
nodejs,
|
||||
yarn,
|
||||
|
||||
# Custom application configuration placed to theme/config.theme.js file
|
||||
# Custom application configuration placed to theme/config.theme.js file.
|
||||
# For the list of available configuration options see
|
||||
# https://github.com/osm-search/nominatim-ui/blob/master/dist/config.defaults.js
|
||||
customConfig ? null,
|
||||
@ -83,6 +84,10 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
passthru.tests = {
|
||||
inherit (nixosTests) nominatim;
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Debugging user interface for Nominatim geocoder";
|
||||
homepage = "https://github.com/osm-search/nominatim-ui";
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
python3Packages,
|
||||
|
||||
nominatim, # required for testVersion
|
||||
nixosTests,
|
||||
testers,
|
||||
}:
|
||||
|
||||
@ -64,8 +65,9 @@ python3Packages.buildPythonApplication rec {
|
||||
|
||||
pythonImportsCheck = [ "nominatim_db" ];
|
||||
|
||||
passthru = {
|
||||
tests.version = testers.testVersion { package = nominatim; };
|
||||
passthru.tests = {
|
||||
version = testers.testVersion { package = nominatim; };
|
||||
inherit (nixosTests) nominatim;
|
||||
};
|
||||
|
||||
meta = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user