]> git.ipfire.org Git - oddments/fireinfo.git/blob - src/fireinfo/system.py
virt: Fix off-by-one error when detecting hypervisor
[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 h = hashlib.sha1(public_id.encode())
193 return h.hexdigest()
194
195 @property
196 def private_id(self):
197 """
198 The private ID is built out of the _unique_id and used to
199 permit a host to do changes on the database.
200
201 No one could ever guess this without access to the host.
202 """
203 private_id = ""
204 for i in reversed(self.secret_id):
205 private_id += i
206
207 if not private_id:
208 return "0" * 40
209
210 h = hashlib.sha1(private_id.encode())
211 return h.hexdigest()
212
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 h = hashlib.sha1(self._unique_id.encode())
223 return h.hexdigest()
224
225 @property
226 def _unique_id(self):
227 """
228 This is a helper ID which is generated out of some hardware information
229 that is considered to be constant over a PC's lifetime.
230
231 None of the data here is ever sent to the server.
232 """
233 ids = []
234
235 # Virtual machines (for example) and some boards have a UUID
236 # which is globally unique.
237 for file in ("product_uuid", "product_serial", "chassis_serial"):
238 id = read_from_file(os.path.join(SYS_CLASS_DMI, file))
239 ids.append(id)
240
241 # Sort out all bogous or invalid strings from the list.
242 _ids = []
243 for id in ids:
244 if id is None:
245 continue
246
247 for i in INVALID_ID_STRINGS_EXACT_MATCH:
248 if id == i:
249 id = None
250 break
251
252 if id:
253 for i in INVALID_ID_STRINGS:
254 if i in id:
255 id = None
256 break
257
258 # Check if the string only contains 0xff
259 if id and all((e == "\xff" for e in id)):
260 id = None
261
262 if id:
263 _ids.append(id)
264
265 ids = _ids
266
267 # Use serial number from root disk (if available) and if
268 # no other ID was found, yet.
269 if not ids:
270 root_disk_serial = self.root_disk_serial
271 if root_disk_serial and not root_disk_serial.startswith("QM000"):
272 ids.append(root_disk_serial)
273
274 # As last resort, we use the UUID from pakfire.
275 if not ids:
276 id = read_from_file("/opt/pakfire/db/uuid")
277 ids.append(id)
278
279 return "#".join(ids)
280
281 @property
282 def language(self):
283 """
284 Return the language code of IPFire or "unknown" if we cannot get it.
285 """
286 # Return "unknown" if settings file does not exist.
287 filename = "/var/ipfire/main/settings"
288 if not os.path.exists(filename):
289 return "unknown"
290
291 with open(filename, "r") as f:
292 for line in f.readlines():
293 key, val = line.split("=", 1)
294 if key == "LANGUAGE":
295 return val.strip()
296
297 @property
298 def release(self):
299 """
300 Return the system release string.
301 """
302 return read_from_file("/etc/system-release") or "unknown"
303
304 @property
305 def bios_vendor(self):
306 """
307 Return the bios vendor name.
308 """
309 return read_from_file("/sys/class/dmi/id/bios_vendor")
310
311 def vendor_model_tuple(self):
312 try:
313 s = self.__cpuinfo["Hardware"]
314 except KeyError:
315 return (None, None)
316
317 if s.startswith("ARM-Versatile"):
318 return ("ARM", s)
319
320 try:
321 v, m = s.split(" ", 1)
322 except ValueError:
323 if s.startswith("BCM"):
324 v = "Broadcom"
325 m = s
326 else:
327 v = None
328 m = s
329
330 return v, m
331
332 @staticmethod
333 def escape_string(s):
334 """
335 Will remove all non-printable characters from the given string
336 """
337 if s is None:
338 return
339
340 return "".join([x for x in s if x in string.printable])
341
342 @property
343 def vendor(self):
344 """
345 Return the vendor string of this system (if any).
346 """
347 ret = None
348 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
349 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
350 if ret:
351 return self.escape_string(ret)
352
353 if os.path.exists("/proc/device-tree"):
354 ret = self.__cpuinfo.get("Hardware", None)
355 else:
356 ret, m = self.vendor_model_tuple()
357
358 return self.escape_string(ret)
359
360 @property
361 def model(self):
362 """
363 Return the model string of this system (if any).
364 """
365 ret = None
366 for file in ("product_name", "board_model", "chassis_model",):
367 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
368 if ret:
369 return self.escape_string(ret)
370
371 # Read device-tree model if available
372 ret = read_from_file("/proc/device-tree/model")
373 if ret:
374 # replace the NULL byte with which the DT string ends
375 ret = ret.replace("\u0000", "")
376
377 # Fall back to read /proc/cpuinfo
378 if not ret:
379 v, ret = self.vendor_model_tuple()
380
381 return self.escape_string(ret)
382
383 @property
384 def memory(self):
385 """
386 Return the amount of memory in kilobytes.
387 """
388 with open("/proc/meminfo", "r") as f:
389 firstline = f.readline().strip()
390 return int(firstline.split()[1])
391
392 @property
393 def kernel_release(self):
394 """
395 Return the kernel release string.
396 """
397 return os.uname()[2]
398
399 @property
400 def root_disk(self):
401 """
402 Return the dev node of the root disk.
403 """
404 with open("/proc/mounts", "r") as f:
405 for line in f.readlines():
406 # Skip empty lines
407 if not line:
408 continue
409
410 dev, mountpoint, fs, rest = line.split(" ", 3)
411 if mountpoint == "/" and not fs == "rootfs":
412 # Cut off /dev
413 dev = dev[5:]
414
415 # Handle raids and MMC cards like (mmcblk0p3).
416 if dev[-2] == "p":
417 return dev[:-2]
418
419 # Otherwise cut off all digits at end of string
420 while dev[-1] in string.digits:
421 dev = dev[:-1]
422
423 return dev
424
425 @property
426 def root_size(self):
427 """
428 Return the size of the root disk in kilobytes.
429 """
430 path = "/sys/block/%s/size" % self.root_disk
431 if not os.path.exists(path):
432 return
433
434 with open(path, "r") as f:
435 return int(f.readline()) * 512 / 1024
436
437 @property
438 def root_disk_serial(self):
439 """
440 Return the serial number of the root disk (if any).
441 """
442 try:
443 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
444 except OSError:
445 return
446
447 if serial:
448 # Strip all spaces
449 return serial.strip()
450
451 def scan(self):
452 """
453 Scan for all devices (PCI/USB) in the system and append them
454 to our list.
455 """
456 self.devices = []
457
458 toscan = (
459 ("/sys/bus/pci/devices", device.PCIDevice),
460 ("/sys/bus/usb/devices", device.USBDevice)
461 )
462 for path, cls in toscan:
463 if not os.path.exists(path):
464 continue
465
466 dirlist = os.listdir(path)
467 for dir in dirlist:
468 self.devices.append(cls(os.path.join(path, dir)))
469
470 @property
471 def virtual(self):
472 """
473 Say if the host is running in a virtual environment.
474 """
475 return self.hypervisor.virtual
476
477 @property
478 def network(self):
479 """
480 Reference to the network class.
481 """
482 return network.Network()
483
484
485 if __name__ == "__main__":
486 s=System()
487 print(s.arch)
488 print(s.language)
489 print(s.release)
490 print(s.bios_vendor)
491 print(s.memory)
492 print(s.kernel)
493 print(s.root_disk)
494 print(s.root_size)
495 print("------------\n", s.devices, "\n------------\n")
496 print(json.dumps(s.profile(), sort_keys=True, indent=4))