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