]> git.openstreetmap.org Git - nominatim.git/blob - nominatim/tools/exec_utils.py
Merge pull request #2903 from lonvia/migration-for-index-reorganization
[nominatim.git] / nominatim / tools / exec_utils.py
1 # SPDX-License-Identifier: GPL-2.0-only
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2022 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions for executing external programs.
9 """
10 from typing import Any, Union, Optional, Mapping, IO
11 from pathlib import Path
12 import logging
13 import os
14 import subprocess
15 import urllib.request as urlrequest
16 from urllib.parse import urlencode
17
18 from nominatim.typing import StrPath
19 from nominatim.version import version_str
20 from nominatim.db.connection import get_pg_env
21
22 LOG = logging.getLogger()
23
24 def run_legacy_script(script: StrPath, *args: Union[int, str],
25                       nominatim_env: Any,
26                       throw_on_fail: bool = False) -> int:
27     """ Run a Nominatim PHP script with the given arguments.
28
29         Returns the exit code of the script. If `throw_on_fail` is True
30         then throw a `CalledProcessError` on a non-zero exit.
31     """
32     cmd = ['/usr/bin/env', 'php', '-Cq',
33            str(nominatim_env.phplib_dir / 'admin' / script)]
34     cmd.extend([str(a) for a in args])
35
36     env = nominatim_env.config.get_os_env()
37     env['NOMINATIM_DATADIR'] = str(nominatim_env.data_dir)
38     env['NOMINATIM_SQLDIR'] = str(nominatim_env.sqllib_dir)
39     env['NOMINATIM_CONFIGDIR'] = str(nominatim_env.config_dir)
40     env['NOMINATIM_DATABASE_MODULE_SRC_PATH'] = str(nominatim_env.module_dir)
41     if not env['NOMINATIM_OSM2PGSQL_BINARY']:
42         env['NOMINATIM_OSM2PGSQL_BINARY'] = str(nominatim_env.osm2pgsql_path)
43
44     proc = subprocess.run(cmd, cwd=str(nominatim_env.project_dir), env=env,
45                           check=throw_on_fail)
46
47     return proc.returncode
48
49 def run_api_script(endpoint: str, project_dir: Path,
50                    extra_env: Optional[Mapping[str, str]] = None,
51                    phpcgi_bin: Optional[Path] = None,
52                    params: Optional[Mapping[str, Any]] = None) -> int:
53     """ Execute a Nominatim API function.
54
55         The function needs a project directory that contains the website
56         directory with the scripts to be executed. The scripts will be run
57         using php_cgi. Query parameters can be added as named arguments.
58
59         Returns the exit code of the script.
60     """
61     log = logging.getLogger()
62     webdir = str(project_dir / 'website')
63     query_string = urlencode(params or {})
64
65     env = dict(QUERY_STRING=query_string,
66                SCRIPT_NAME=f'/{endpoint}.php',
67                REQUEST_URI=f'/{endpoint}.php?{query_string}',
68                CONTEXT_DOCUMENT_ROOT=webdir,
69                SCRIPT_FILENAME=f'{webdir}/{endpoint}.php',
70                HTTP_HOST='localhost',
71                HTTP_USER_AGENT='nominatim-tool',
72                REMOTE_ADDR='0.0.0.0',
73                DOCUMENT_ROOT=webdir,
74                REQUEST_METHOD='GET',
75                SERVER_PROTOCOL='HTTP/1.1',
76                GATEWAY_INTERFACE='CGI/1.1',
77                REDIRECT_STATUS='CGI')
78
79     if extra_env:
80         env.update(extra_env)
81
82     if phpcgi_bin is None:
83         cmd = ['/usr/bin/env', 'php-cgi']
84     else:
85         cmd = [str(phpcgi_bin)]
86
87     proc = subprocess.run(cmd, cwd=str(project_dir), env=env,
88                           stdout=subprocess.PIPE,
89                           stderr=subprocess.PIPE,
90                           check=False)
91
92     if proc.returncode != 0 or proc.stderr:
93         if proc.stderr:
94             log.error(proc.stderr.decode('utf-8').replace('\\n', '\n'))
95         else:
96             log.error(proc.stdout.decode('utf-8').replace('\\n', '\n'))
97         return proc.returncode or 1
98
99     result = proc.stdout.decode('utf-8')
100     content_start = result.find('\r\n\r\n')
101
102     print(result[content_start + 4:].replace('\\n', '\n'))
103
104     return 0
105
106
107 def run_php_server(server_address: str, base_dir: StrPath) -> None:
108     """ Run the built-in server from the given directory.
109     """
110     subprocess.run(['/usr/bin/env', 'php', '-S', server_address],
111                    cwd=str(base_dir), check=True)
112
113
114 def run_osm2pgsql(options: Mapping[str, Any]) -> None:
115     """ Run osm2pgsql with the given options.
116     """
117     env = get_pg_env(options['dsn'])
118     cmd = [str(options['osm2pgsql']),
119            '--hstore', '--latlon', '--slim',
120            '--log-progress', 'true',
121            '--number-processes', str(options['threads']),
122            '--cache', str(options['osm2pgsql_cache']),
123            '--style', str(options['osm2pgsql_style'])
124           ]
125
126     if str(options['osm2pgsql_style']).endswith('.lua'):
127         env['LUA_PATH'] = ';'.join((str(options['osm2pgsql_style_path'] / 'flex-base.lua'),
128                                     os.environ.get('LUAPATH', ';')))
129         cmd.extend(('--output', 'flex'))
130     else:
131         cmd.extend(('--output', 'gazetteer'))
132
133     cmd.append('--append' if options['append'] else '--create')
134
135     if options['flatnode_file']:
136         cmd.extend(('--flat-nodes', options['flatnode_file']))
137
138     if not options.get('forward_dependencies', False):
139         cmd.extend(('--with-forward-dependencies', 'false'))
140
141     for key, param in (('slim_data', '--tablespace-slim-data'),
142                        ('slim_index', '--tablespace-slim-index'),
143                        ('main_data', '--tablespace-main-data'),
144                        ('main_index', '--tablespace-main-index')):
145         if options['tablespaces'][key]:
146             cmd.extend((param, options['tablespaces'][key]))
147
148     if options.get('disable_jit', False):
149         env['PGOPTIONS'] = '-c jit=off -c max_parallel_workers_per_gather=0'
150
151     if 'import_data' in options:
152         cmd.extend(('-r', 'xml', '-'))
153     elif isinstance(options['import_file'], list):
154         for fname in options['import_file']:
155             cmd.append(str(fname))
156     else:
157         cmd.append(str(options['import_file']))
158
159     subprocess.run(cmd, cwd=options.get('cwd', '.'),
160                    input=options.get('import_data'),
161                    env=env, check=True)
162
163
164 def get_url(url: str) -> str:
165     """ Get the contents from the given URL and return it as a UTF-8 string.
166     """
167     headers = {"User-Agent": f"Nominatim/{version_str()}"}
168
169     try:
170         request = urlrequest.Request(url, headers=headers)
171         with urlrequest.urlopen(request) as response: # type: IO[bytes]
172             return response.read().decode('utf-8')
173     except Exception:
174         LOG.fatal('Failed to load URL: %s', url)
175         raise