]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_db/tools/collect_os_info.py
improve error message on missing modules from nominatim_api
[nominatim.git] / src / nominatim_db / tools / collect_os_info.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Collection of host system information including software versions, memory,
9 storage, and database configuration.
10 """
11 import os
12 import subprocess
13 import sys
14 from pathlib import Path
15 from typing import List, Optional, Union
16
17 import psutil
18
19 from ..config import Configuration
20 from ..db.connection import connect, server_version_tuple, execute_scalar
21 from ..version import NOMINATIM_VERSION
22
23
24 def friendly_memory_string(mem: float) -> str:
25     """Create a user friendly string for the amount of memory specified as mem"""
26     mem_magnitude = ("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
27     mag = 0
28     # determine order of magnitude
29     while mem > 1000:
30         mem /= 1000
31         mag += 1
32
33     return f"{mem:.1f} {mem_magnitude[mag]}"
34
35
36 def run_command(cmd: Union[str, List[str]]) -> str:
37     """Runs a command using the shell and returns the output from stdout"""
38     try:
39         if sys.version_info < (3, 7):
40             cap_out = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
41         else:
42             cap_out = subprocess.run(cmd, capture_output=True, check=False)
43         return cap_out.stdout.decode("utf-8")
44     except FileNotFoundError:
45         # non-Linux system should end up here
46         return f"Unknown (unable to find the '{cmd}' command)"
47
48
49 def os_name_info() -> str:
50     """Obtain Operating System Name (and possibly the version)"""
51     os_info = None
52     # man page os-release(5) details meaning of the fields
53     if Path("/etc/os-release").is_file():
54         os_info = from_file_find_line_portion(
55             "/etc/os-release", "PRETTY_NAME", "=")
56     # alternative location
57     elif Path("/usr/lib/os-release").is_file():
58         os_info = from_file_find_line_portion(
59             "/usr/lib/os-release", "PRETTY_NAME", "="
60         )
61
62     # fallback on Python's os name
63     if os_info is None or os_info == "":
64         os_info = os.name
65
66     # if the above is insufficient, take a look at neofetch's approach to OS detection
67     return os_info
68
69
70 # Note: Intended to be used on informational files like /proc
71 def from_file_find_line_portion(
72     filename: str, start: str, sep: str, fieldnum: int = 1
73 ) -> Optional[str]:
74     """open filename, finds the line starting with the 'start' string.
75     Splits the line using separator and returns a "fieldnum" from the split."""
76     with open(filename, encoding='utf8') as file:
77         result = ""
78         for line in file:
79             if line.startswith(start):
80                 result = line.split(sep)[fieldnum].strip()
81         return result
82
83
84 def get_postgresql_config(version: int) -> str:
85     """Retrieve postgres configuration file"""
86     try:
87         with open(f"/etc/postgresql/{version}/main/postgresql.conf", encoding='utf8') as file:
88             db_config = file.read()
89             file.close()
90             return db_config
91     except IOError:
92         return f"**Could not read '/etc/postgresql/{version}/main/postgresql.conf'**"
93
94
95 def report_system_information(config: Configuration) -> None:
96     """Generate a report about the host system including software versions, memory,
97     storage, and database configuration."""
98
99     with connect(config.get_libpq_dsn(), dbname='postgres') as conn:
100         postgresql_ver: str = '.'.join(map(str, server_version_tuple(conn)))
101
102         with conn.cursor() as cur:
103             cur.execute("SELECT datname FROM pg_catalog.pg_database WHERE datname=%s",
104                         (config.get_database_params()['dbname'], ))
105             nominatim_db_exists = cur.rowcount > 0
106
107     if nominatim_db_exists:
108         with connect(config.get_libpq_dsn()) as conn:
109             postgis_ver: str = execute_scalar(conn, 'SELECT postgis_lib_version()')
110     else:
111         postgis_ver = "Unable to connect to database"
112
113     postgresql_config: str = get_postgresql_config(int(float(postgresql_ver)))
114
115     # Note: psutil.disk_partitions() is similar to run_command("lsblk")
116
117     # Note: run_command("systemd-detect-virt") only works on Linux, on other OSes
118     # should give a message: "Unknown (unable to find the 'systemd-detect-virt' command)"
119
120     # Generates the Markdown report.
121
122     report = f"""
123     **Instructions**
124     Use this information in your issue report at https://github.com/osm-search/Nominatim/issues
125     Redirect the output to a file:
126     $ ./collect_os_info.py > report.md
127
128
129     **Software Environment:**
130     - Python version: {sys.version}
131     - Nominatim version: {NOMINATIM_VERSION!s}
132     - PostgreSQL version: {postgresql_ver}
133     - PostGIS version: {postgis_ver}
134     - OS: {os_name_info()}
135     
136     
137     **Hardware Configuration:**
138     - RAM: {friendly_memory_string(psutil.virtual_memory().total)}
139     - number of CPUs: {psutil.cpu_count(logical=False)}
140     - bare metal/AWS/other cloud service (per systemd-detect-virt(1)):
141         {run_command("systemd-detect-virt")}
142     - type and size of disks:
143     **`df -h` - df - report file system disk space usage: **
144     ```
145     {run_command(["df", "-h"])}
146     ```
147     
148     **lsblk - list block devices: **
149     ```
150     {run_command("lsblk")}
151     ```
152     
153     
154     **Postgresql Configuration:**
155     ```
156     {postgresql_config}
157     ```
158     **Notes**
159     Please add any notes about anything above anything above that is incorrect.
160 """
161     print(report)