]> git.ipfire.org Git - oddments/fireinfo.git/blame - src/fireinfo/system.py
Run 2to3
[oddments/fireinfo.git] / src / fireinfo / system.py
CommitLineData
b45f0e98 1#!/usr/bin/python
3b5ed4e1
MT
2###############################################################################
3# #
4# Fireinfo #
5# Copyright (C) 2010, 2011 IPFire Team (www.ipfire.org) #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
b45f0e98 21
31a30328 22import hashlib
b45f0e98
MT
23import json
24import os
25import string
26
3f70e7fd
MT
27import _fireinfo
28
3b72291d
MT
29from . import bios
30from . import cpu
31from . import device
32from . import hypervisor
33from . import network
b45f0e98 34
0c5ef738
MT
35PROFILE_VERSION = 0
36
45b74ad5 37SYS_CLASS_DMI = "/sys/class/dmi/id"
73014efe 38SECRET_ID_FILE = "/etc/fireinfo-id"
45b74ad5 39
7eadbfba
MT
40INVALID_ID_STRINGS = (
41 "OEM", "O.E.M.", "o.e.m.",
42 "N/A", "n/a",
74ce4c8d 43 "12345", "54321", "202020",
ed491340 44 "Chassis", "chassis",
d1f67891 45 "Default string",
80365bb3 46 "EVAL",
40cffee0 47 "Not Applicable",
dafc2399 48 "None", "empty",
edacae4b 49 "Serial", "System Serial Number",
220ffe76 50 "XXXXX",
40cffee0 51 "01010101-0101-0101-0101-010101010101",
ac68f90a 52 "00020003-0004-0005-0006-000700080009",
92b4cddf 53 "03000200-0400-0500-0006-000700080009",
80365bb3 54 "11111111-1111-1111-1111-111111111111",
2689ff71 55 "0000000", "00000000",
7eadbfba
MT
56)
57
220ffe76
MT
58INVALID_ID_STRINGS_EXACT_MATCH = (
59 "NA",
60)
61
32aeec73
MT
62class Singleton(type):
63 def __init__(cls, name, bases, dict):
64 super(Singleton, cls).__init__(name, bases, dict)
65 cls.instance = None
66
67 def __call__(cls, *args, **kw):
68 if cls.instance is None:
69 cls.instance = super(Singleton, cls).__call__(*args, **kw)
70
71 return cls.instance
72
73
45b74ad5
MT
74def read_from_file(filename):
75 """
76 Read all data from filename.
77 """
78 if not os.path.exists(filename):
79 return
80
81 try:
82 with open(filename) as f:
83 return f.read().strip()
84 except IOError:
85 pass
86
3b72291d 87class System(object, metaclass=Singleton):
b45f0e98 88 def __init__(self):
e1ca6671
MT
89 self.bios = bios.BIOS(self)
90
b45f0e98
MT
91 # find all devices
92 self.devices = []
93 self.scan()
94 self.cpu = cpu.CPU()
715ba5ac
MT
95 self.hypervisor = hypervisor.Hypervisor()
96
3f4e98ab
MT
97 # Read /proc/cpuinfo for vendor information.
98 self.__cpuinfo = self.cpu.read_cpuinfo()
99
b45f0e98 100 def profile(self):
0c5ef738
MT
101 p = {}
102 p["system"] = {
45b74ad5
MT
103 # System information
104 "model" : self.model,
105 "vendor" : self.vendor,
106
0c5ef738
MT
107 # Indicator if the system is running in a
108 # virtual environment.
d8614fc3 109 "virtual" : self.virtual,
b45f0e98 110
0c5ef738
MT
111 # System language
112 "language" : self.language,
113
114 # Release information
115 "release" : self.release,
116 "kernel_release" : self.kernel_release,
117
118 "memory" : self.memory,
119 "root_size" : self.root_size,
120 }
121
122 p["devices"] = []
b45f0e98 123 for device in self.devices:
97679775 124 d = {
b45f0e98
MT
125 "subsystem" : device.subsystem.lower(),
126 "vendor" : device.vendor.lower(),
127 "model" : device.model.lower(),
7923840b
MT
128 "deviceclass" : device.deviceclass,
129 "driver" : device.driver,
97679775
MT
130 }
131
132 # PCI devices provide subsystem information, USB don't.
133 if d["subsystem"] == "pci":
134 d["sub_model"] = device.sub_model
135 d["sub_vendor"] = device.sub_vendor
136
137 p["devices"].append(d)
138
b45f0e98 139 p["cpu"] = {
0c5ef738 140 "arch" : self.arch,
b45f0e98
MT
141 "vendor" : self.cpu.vendor,
142 "model" : self.cpu.model,
eecf2eea 143 "model_string" : self.cpu.model_string,
b45f0e98
MT
144 "stepping" : self.cpu.stepping,
145 "flags" : self.cpu.flags,
b45f0e98 146 "speed" : self.cpu.speed,
b45f0e98
MT
147 "family" : self.cpu.family,
148 "count" : self.cpu.count
149 }
715ba5ac 150
a9401d95
MT
151 if self.cpu.bogomips:
152 p["bogomips"] = self.cpu.bogomips
153
4cb61965
MT
154 p["network"] = {
155 "green" : self.network.has_green(),
156 "blue" : self.network.has_blue(),
157 "orange" : self.network.has_orange(),
158 "red" : self.network.has_red(),
159 }
160
715ba5ac
MT
161 # Only append hypervisor information if we are virtualized.
162 if self.virtual:
163 p["hypervisor"] = {
715ba5ac
MT
164 "vendor" : self.hypervisor.vendor,
165 }
166
0c5ef738
MT
167 return {
168 # Profile version
169 "profile_version" : PROFILE_VERSION,
170
171 # Identification and authorization codes
172 "public_id" : self.public_id,
173 "private_id" : self.private_id,
174
175 # Actual profile data
176 "profile" : p,
177 }
b45f0e98
MT
178
179
180 @property
181 def arch(self):
182 return os.uname()[4]
183
184 @property
185 def public_id(self):
31a30328
MT
186 """
187 This returns a globally (hopefully) ID to identify the host
188 later (by request) in the database.
189 """
73014efe 190 public_id = self.secret_id
3d10b6d9
MT
191 if not public_id:
192 return "0" * 40
193
194 return hashlib.sha1(public_id).hexdigest()
195
b45f0e98
MT
196 @property
197 def private_id(self):
31a30328
MT
198 """
199 The private ID is built out of the _unique_id and used to
200 permit a host to do changes on the database.
201
202 No one could ever guess this without access to the host.
203 """
5e2ba24e 204 private_id = ""
73014efe 205 for i in reversed(self.secret_id):
5e2ba24e
MT
206 private_id += i
207
3d10b6d9
MT
208 if not private_id:
209 return "0" * 40
210
5e2ba24e 211 return hashlib.sha1(private_id).hexdigest()
31a30328 212
73014efe
MT
213 @property
214 def secret_id(self):
215 """
216 Read a "secret" ID from a file if available
217 or calculate it from the hardware.
218 """
219 if os.path.exists(SECRET_ID_FILE):
220 return read_from_file(SECRET_ID_FILE)
221
222 return hashlib.sha1(self._unique_id).hexdigest()
223
31a30328
MT
224 @property
225 def _unique_id(self):
226 """
227 This is a helper ID which is generated out of some hardware information
228 that is considered to be constant over a PC's lifetime.
229
230 None of the data here is ever sent to the server.
231 """
ec65e266 232 ids = []
3d10b6d9
MT
233
234 # Virtual machines (for example) and some boards have a UUID
235 # which is globally unique.
236 for file in ("product_uuid", "product_serial", "chassis_serial"):
7eadbfba 237 id = read_from_file(os.path.join(SYS_CLASS_DMI, file))
c0ef2eb7 238 ids.append(id)
3d10b6d9 239
7eadbfba
MT
240 # Sort out all bogous or invalid strings from the list.
241 _ids = []
242 for id in ids:
88b0ded5
MT
243 if id is None:
244 continue
245
220ffe76
MT
246 for i in INVALID_ID_STRINGS_EXACT_MATCH:
247 if id == i:
c0ef2eb7 248 id = None
7eadbfba
MT
249 break
250
220ffe76
MT
251 if id:
252 for i in INVALID_ID_STRINGS:
253 if i in id:
254 id = None
255 break
256
d58f8ef7 257 # Check if the string only contains 0xff
deafec98 258 if id and all((e == "\xff" for e in id)):
d58f8ef7
MT
259 id = None
260
7eadbfba
MT
261 if id:
262 _ids.append(id)
263
264 ids = _ids
265
17dc2486
MT
266 # Use serial number from root disk (if available) and if
267 # no other ID was found, yet.
268 if not ids:
269 root_disk_serial = self.root_disk_serial
4ea06541 270 if root_disk_serial and not root_disk_serial.startswith("QM000"):
17dc2486 271 ids.append(root_disk_serial)
121f9f20 272
f05eac0d
MT
273 # As last resort, we use the UUID from pakfire.
274 if not ids:
275 id = read_from_file("/opt/pakfire/db/uuid")
276 ids.append(id)
277
ec65e266 278 return "#".join(ids)
31a30328 279
b45f0e98
MT
280 @property
281 def language(self):
e8221cde
MT
282 """
283 Return the language code of IPFire or "unknown" if we cannot get it.
284 """
b3ea53a7
MT
285 # Return "unknown" if settings file does not exist.
286 filename = "/var/ipfire/main/settings"
287 if not os.path.exists(filename):
288 return "unknown"
289
290 with open(filename, "r") as f:
b45f0e98
MT
291 for line in f.readlines():
292 key, val = line.split("=", 1)
e8221cde 293 if key == "LANGUAGE":
b45f0e98
MT
294 return val.strip()
295
296 @property
297 def release(self):
e8221cde
MT
298 """
299 Return the system release string.
300 """
3f4e98ab 301 return read_from_file("/etc/system-release") or "unknown"
b56bde73
SP
302
303 @property
304 def bios_vendor(self):
e8221cde
MT
305 """
306 Return the bios vendor name.
307 """
770c8841 308 return read_from_file("/sys/class/dmi/id/bios_vendor")
b56bde73 309
810fe432
MT
310 def vendor_model_tuple(self):
311 try:
312 s = self.__cpuinfo["Hardware"]
313 except KeyError:
314 return (None, None)
315
316 if s.startswith("ARM-Versatile"):
317 return ("ARM", s)
318
319 try:
320 v, m = s.split(" ", 1)
321 except ValueError:
322 if s.startswith("BCM"):
323 v = "Broadcom"
324 m = s
325 else:
326 v = None
327 m = s
328
329 return v, m
330
4468fb2e
MT
331 @staticmethod
332 def escape_string(s):
333 """
334 Will remove all non-printable characters from the given string
335 """
336 if s is None:
337 return
338
3b72291d 339 return [x for x in s if x in string.printable]
4468fb2e 340
45b74ad5
MT
341 @property
342 def vendor(self):
e8221cde
MT
343 """
344 Return the vendor string of this system (if any).
345 """
45b74ad5 346 ret = None
240fefea 347 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
45b74ad5
MT
348 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
349 if ret:
4468fb2e 350 return self.escape_string(ret)
45b74ad5 351
3383941d
MT
352 if os.path.exists("/proc/device-tree"):
353 ret = self.__cpuinfo.get("Hardware", None)
354 else:
355 ret, m = self.vendor_model_tuple()
3f4e98ab 356
4468fb2e 357 return self.escape_string(ret)
45b74ad5
MT
358
359 @property
360 def model(self):
e8221cde
MT
361 """
362 Return the model string of this system (if any).
363 """
45b74ad5 364 ret = None
84efbcb4 365 for file in ("product_name", "board_model", "chassis_model",):
45b74ad5
MT
366 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
367 if ret:
4468fb2e 368 return self.escape_string(ret)
45b74ad5 369
b9a068e2
MT
370 # Read device-tree model if available
371 ret = read_from_file("/proc/device-tree/model")
372 if ret:
373 # replace the NULL byte with which the DT string ends
3b72291d 374 ret = ret.replace("\u0000", "")
3f4e98ab 375
810fe432
MT
376 # Fall back to read /proc/cpuinfo
377 if not ret:
378 v, ret = self.vendor_model_tuple()
379
4468fb2e 380 return self.escape_string(ret)
45b74ad5 381
b45f0e98
MT
382 @property
383 def memory(self):
e8221cde
MT
384 """
385 Return the amount of memory in kilobytes.
386 """
b45f0e98
MT
387 with open("/proc/meminfo", "r") as f:
388 firstline = f.readline().strip()
2ced5413 389 return int(firstline.split()[1])
b45f0e98
MT
390
391 @property
0c5ef738 392 def kernel_release(self):
e8221cde
MT
393 """
394 Return the kernel release string.
395 """
b45f0e98
MT
396 return os.uname()[2]
397
398 @property
399 def root_disk(self):
e8221cde
MT
400 """
401 Return the dev node of the root disk.
402 """
1565fbf8
MT
403 with open("/proc/mounts", "r") as f:
404 for line in f.readlines():
405 # Skip empty lines
406 if not line:
407 continue
408
409 dev, mountpoint, fs, rest = line.split(" ", 3)
410 if mountpoint == "/" and not fs == "rootfs":
411 # Cut off /dev
412 dev = dev[5:]
cf062e0b 413
1565fbf8
MT
414 # Handle raids and MMC cards like (mmcblk0p3).
415 if dev[-2] == "p":
416 return dev[:-2]
cf062e0b 417
1565fbf8
MT
418 # Otherwise cut off all digits at end of string
419 while dev[-1] in string.digits:
420 dev = dev[:-1]
e8221cde 421
1565fbf8 422 return dev
b45f0e98
MT
423
424 @property
425 def root_size(self):
e8221cde
MT
426 """
427 Return the size of the root disk in kilobytes.
428 """
429 path = "/sys/block/%s/size" % self.root_disk
b45f0e98
MT
430 if not os.path.exists(path):
431 return
e8221cde 432
b45f0e98 433 with open(path, "r") as f:
e8221cde 434 return int(f.readline()) * 512 / 1024
3f70e7fd
MT
435
436 @property
437 def root_disk_serial(self):
e8221cde
MT
438 """
439 Return the serial number of the root disk (if any).
440 """
7890c775
MT
441 try:
442 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
443 except OSError:
444 return
e658b1a0
MT
445
446 if serial:
447 # Strip all spaces
448 return serial.strip()
449
b45f0e98 450 def scan(self):
e8221cde
MT
451 """
452 Scan for all devices (PCI/USB) in the system and append them
453 to our list.
454 """
455 self.devices = []
456
457 toscan = (
458 ("/sys/bus/pci/devices", device.PCIDevice),
459 ("/sys/bus/usb/devices", device.USBDevice)
460 )
b45f0e98 461 for path, cls in toscan:
94700953
MT
462 if not os.path.exists(path):
463 continue
464
b45f0e98
MT
465 dirlist = os.listdir(path)
466 for dir in dirlist:
467 self.devices.append(cls(os.path.join(path, dir)))
d8614fc3
MT
468
469 @property
470 def virtual(self):
471 """
472 Say if the host is running in a virtual environment.
473 """
715ba5ac 474 return self.hypervisor.virtual
b45f0e98 475
4cb61965
MT
476 @property
477 def network(self):
478 """
479 Reference to the network class.
480 """
481 return network.Network()
482
b45f0e98
MT
483
484if __name__ == "__main__":
485 s=System()
3b72291d
MT
486 print(s.arch)
487 print(s.language)
488 print(s.release)
489 print(s.bios_vendor)
490 print(s.memory)
491 print(s.kernel)
492 print(s.root_disk)
493 print(s.root_size)
494 print("------------\n", s.devices, "\n------------\n")
495 print(json.dumps(s.profile(), sort_keys=True, indent=4))