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