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