]> git.openstreetmap.org Git - nominatim.git/blob - utils/collect_os_info.py
0f5bf04b63cfb5d8713d3a88971c98d97bdebdd3
[nominatim.git] / utils / collect_os_info.py
1
2 import os
3 from pathlib import Path
4 import subprocess
5 import sys
6 from typing import Optional, Union
7
8 # external requirement
9 import psutil
10
11 # from nominatim.version import NOMINATIM_VERSION
12 # from nominatim.db.connection import connect
13
14
15 class ReportSystemInformation:
16         """Generate a report about the host system including software versions, memory,
17            storage, and database configuration."""
18         def __init__(self):
19                 self._memory: int = psutil.virtual_memory().total
20                 self.friendly_memory: str = self._friendly_memory_string(self._memory)
21                 # psutil.cpu_count(logical=False) returns the number of CPU cores.
22                 # For number of logical cores (Hypthreaded), call psutil.cpu_count() or os.cpu_count() 
23                 self.num_cpus: int = psutil.cpu_count(logical=False)
24                 self.os_info: str = self._os_name_info()
25
26 ### These are commented out because they have not been tested.
27 #               self.nominatim_ver: str = '{0[0]}.{0[1]}.{0[2]}-{0[3]}'.format(NOMINATIM_VERSION)
28 #       self._pg_version = conn.server_version_tuple()
29 #       self._postgis_version = conn.postgis_version_tuple()
30 #               self.postgresql_ver: str = self._convert_version(self._pg_version)
31 #               self.postgis_ver: str = self._convert_version(self._postgis_version)
32
33                 self.nominatim_ver: str = ""
34                 self.postgresql_ver: str = ""
35                 self.postgresql_config: str = ""
36                 self.postgis_ver: str = ""
37
38                 # the below commands require calling the shell to gather information
39                 self.disk_free: str = self._run_command(["df", "-h"])
40                 self.lsblk: str = self._run_command("lsblk")
41                 # psutil.disk_partitions() <- this function is similar to the above, but it is cross platform
42
43                 # Note: `systemd-detect-virt` command only works on Linux, on other OSes
44                 # should give a message: "Unknown (unable to find the 'systemd-detect-virt' command)"
45                 self.container_vm_env: str = self._run_command("systemd-detect-virt")
46
47         def _convert_version(self, ver_tup: tuple) -> str:
48                 """converts tuple version (ver_tup) to a string representation"""
49                 return ".".join(map(str,ver_tup))
50
51         def _friendly_memory_string(self, mem: int) -> str:
52                 """Create a user friendly string for the amount of memory specified as mem"""
53                 mem_magnitude = ('bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB')
54                 mag = 0
55                 # determine order of magnitude
56                 while mem > 1000:
57                         mem /= 1000
58                         mag += 1
59                 
60                 return f"{mem:.1f} {mem_magnitude[mag]}"
61
62
63         def _run_command(self, cmd: Union[str, list]) -> str:
64                 """Runs a command using the shell and returns the output from stdout"""
65                 try:
66                         if sys.version_info < (3, 7):
67                                 cap_out = subprocess.run(cmd, stdout=subprocess.PIPE)
68                         else:
69                                 cap_out = subprocess.run(cmd, capture_output=True)
70                         return cap_out.stdout.decode("utf-8")
71                 except FileNotFoundError:
72                                 # non-Linux system should end up here
73                                 return f"Unknown (unable to find the '{cmd}' command)"
74
75
76         def _os_name_info(self) -> str:
77                 """Obtain Operating System Name (and possibly the version)"""
78
79                 os_info = None
80                 # man page os-release(5) details meaning of the fields
81                 if Path("/etc/os-release").is_file():
82                         os_info = self._from_file_find_line_portion("/etc/os-release", "PRETTY_NAME", "=")
83                 # alternative location 
84                 elif Path("/usr/lib/os-release").is_file():
85                         os_info = self._from_file_find_line_portion("/usr/lib/os-release", "PRETTY_NAME", "=")
86
87                 # fallback on Python's os name
88                 if(os_info is None or os_info == ""):
89                         os_info = os.name
90
91                 # if the above is insufficient, take a look at neofetch's approach to OS detection              
92                 return os_info
93
94
95         # Note: Intended to be used on informational files like /proc
96         def _from_file_find_line_portion(self, filename: str, start: str, sep: str,
97                                                                          fieldnum: int = 1) -> Optional[str]:
98                 """open filename, finds the line starting with the 'start' string.
99                    Splits the line using seperator and returns a "fieldnum" from the split."""
100                 with open(filename) as fh:
101                         for line in fh:
102                                 if line.startswith(start):
103                                         result = line.split(sep)[fieldnum].strip()
104                                         return result
105
106         def report(self, out = sys.stdout, err = sys.stderr) -> None:
107                 """Generates the Markdown report. 
108                 
109                 Optionally pass out or err parameters to redirect the output of stdout
110                  and stderr to other file objects."""
111                 
112                 # NOTE: This should be a report format.  Any conversions or lookup has be
113                 #  done, do that action in the __init__() or another function. 
114                 message = """
115 Use this information in your issue report at https://github.com/osm-search/Nominatim/issues
116 Copy and paste or redirect the output of the file:
117     $ ./collect_os_info.py > report.md
118 """
119                 report = f"""
120 **Software Environment:**
121 - Python version: {sys.version}
122 - Nominatim version: {self.nominatim_ver} 
123 - PostgreSQL version: {self.postgresql_ver} 
124 - PostGIS version: {self.postgis_ver}
125 - OS: {self.os_info}
126
127
128 **Hardware Configuration:**
129 - RAM: {self.friendly_memory}
130 - number of CPUs: {self.num_cpus}
131 - bare metal/AWS/other cloud service (per systemd-detect-virt(1)): {self.container_vm_env} 
132 - type and size of disks:
133 **`df -h` - df - report file system disk space usage: **
134 ```
135 {self.disk_free}
136 ```
137
138 **lsblk - list block devices: **
139 ```
140 {self.lsblk}
141 ```
142
143
144 **Postgresql Configuration:**
145 ```
146 {self.postgresql_config}
147 ```
148 **Notes**
149 Please add any notes about anything above anything above that is incorrect.
150         """
151
152                 print(message, file = err)
153                 print(report, file = out)
154
155
156 if __name__ == "__main__":
157         sys_info = ReportSystemInformation()
158         sys_info.report()