1 # SPDX-License-Identifier: GPL-2.0-only
 
   3 # This file is part of Nominatim. (https://nominatim.org)
 
   5 # Copyright (C) 2022 by the Nominatim developer community.
 
   6 # For a full list of authors see the git log.
 
   8 Collection of host system information including software versions, memory,
 
   9 storage, and database configuration.
 
  14 from pathlib import Path
 
  15 from typing import List, Optional, Tuple, Union
 
  18 from psycopg2.extensions import make_dsn, parse_dsn
 
  20 from nominatim.config import Configuration
 
  21 from nominatim.db.connection import connect
 
  22 from nominatim.version import NOMINATIM_VERSION
 
  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))
 
  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")
 
  34     # determine order of magnitude
 
  39     return f"{mem:.1f} {mem_magnitude[mag]}"
 
  42 def run_command(cmd: Union[str, List[str]]) -> str:
 
  43     """Runs a command using the shell and returns the output from stdout"""
 
  45         if sys.version_info < (3, 7):
 
  46             cap_out = subprocess.run(cmd, stdout=subprocess.PIPE, check=False)
 
  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)"
 
  55 def os_name_info() -> str:
 
  56     """Obtain Operating System Name (and possibly the version)"""
 
  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", "="
 
  68     # fallback on Python's os name
 
  69     if os_info is None or os_info == "":
 
  72     # if the above is insufficient, take a look at neofetch's approach to OS detection
 
  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
 
  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:
 
  85             if line.startswith(start):
 
  86                 result = line.split(sep)[fieldnum].strip()
 
  90 def get_postgresql_config(version: int) -> str:
 
  91     """Retrieve postgres configuration file"""
 
  93         with open(f"/etc/postgresql/{version}/main/postgresql.conf", encoding='utf8') as file:
 
  94             db_config = file.read()
 
  98         return f"**Could not read '/etc/postgresql/{version}/main/postgresql.conf'**"
 
 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."""
 
 105     with connect(make_dsn(config.get_libpq_dsn(), dbname='postgres')) as conn:
 
 106         postgresql_ver: str = convert_version(conn.server_version_tuple())
 
 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
 
 113     if nominatim_db_exists:
 
 114         with connect(config.get_libpq_dsn()) as conn:
 
 115             postgis_ver: str = convert_version(conn.postgis_version_tuple())
 
 117         postgis_ver = "Unable to connect to database"
 
 119     postgresql_config: str = get_postgresql_config(int(float(postgresql_ver)))
 
 121     # Note: psutil.disk_partitions() is similar to run_command("lsblk")
 
 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)"
 
 126     # Generates the Markdown report.
 
 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
 
 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()}
 
 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: **
 
 150     {run_command(["df", "-h"])}
 
 153     **lsblk - list block devices: **
 
 155     {run_command("lsblk")}
 
 159     **Postgresql Configuration:**
 
 164     Please add any notes about anything above anything above that is incorrect.