nixos/modules: add nominatim module and test
This commit is contained in:
		
							parent
							
								
									5717441202
								
							
						
					
					
						commit
						5cd09e28ae
					
				| @ -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}/"; | ||||
|               }; | ||||
|             }; | ||||
|           }; | ||||
|         }; | ||||
|       }; | ||||
|     }; | ||||
| } | ||||
| @ -1012,6 +1012,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" | ||||
|     """) | ||||
|   ''; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Ivan Mincik
						Ivan Mincik