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