]> git.ipfire.org Git - oddments/fireinfo.git/blob - src/fireinfo/system.py
Fix crash if there is id has already been reset to None
[oddments/fireinfo.git] / src / fireinfo / system.py
1 #!/usr/bin/python
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 ###############################################################################
21
22 import hashlib
23 import json
24 import os
25 import string
26
27 import _fireinfo
28
29 import bios
30 import cpu
31 import device
32 import hypervisor
33 import network
34
35 PROFILE_VERSION = 0
36
37 SYS_CLASS_DMI = "/sys/class/dmi/id"
38 SECRET_ID_FILE = "/etc/fireinfo-id"
39
40 INVALID_ID_STRINGS = (
41 "OEM", "O.E.M.", "o.e.m.",
42 "N/A", "n/a",
43 "12345", "54321", "202020",
44 "Chassis", "chassis",
45 "EVAL",
46 "Not Applicable",
47 "None", "empty",
48 "Serial", "System Serial Number",
49 "XXXXX",
50 "01010101-0101-0101-0101-010101010101",
51 "00020003-0004-0005-0006-000700080009",
52 "03000200-0400-0500-0006-000700080009",
53 "11111111-1111-1111-1111-111111111111",
54 "0000000", "00000000",
55 )
56
57 INVALID_ID_STRINGS_EXACT_MATCH = (
58 "NA",
59 )
60
61 class Singleton(type):
62 def __init__(cls, name, bases, dict):
63 super(Singleton, cls).__init__(name, bases, dict)
64 cls.instance = None
65
66 def __call__(cls, *args, **kw):
67 if cls.instance is None:
68 cls.instance = super(Singleton, cls).__call__(*args, **kw)
69
70 return cls.instance
71
72
73 def read_from_file(filename):
74 """
75 Read all data from filename.
76 """
77 if not os.path.exists(filename):
78 return
79
80 try:
81 with open(filename) as f:
82 return f.read().strip()
83 except IOError:
84 pass
85
86 class System(object):
87 __metaclass__ = Singleton
88
89 def __init__(self):
90 self.bios = bios.BIOS(self)
91
92 # find all devices
93 self.devices = []
94 self.scan()
95 self.cpu = cpu.CPU()
96 self.hypervisor = hypervisor.Hypervisor()
97
98 # Read /proc/cpuinfo for vendor information.
99 self.__cpuinfo = self.cpu.read_cpuinfo()
100
101 def profile(self):
102 p = {}
103 p["system"] = {
104 # System information
105 "model" : self.model,
106 "vendor" : self.vendor,
107
108 # Indicator if the system is running in a
109 # virtual environment.
110 "virtual" : self.virtual,
111
112 # System language
113 "language" : self.language,
114
115 # Release information
116 "release" : self.release,
117 "kernel_release" : self.kernel_release,
118
119 "memory" : self.memory,
120 "root_size" : self.root_size,
121 }
122
123 p["devices"] = []
124 for device in self.devices:
125 d = {
126 "subsystem" : device.subsystem.lower(),
127 "vendor" : device.vendor.lower(),
128 "model" : device.model.lower(),
129 "deviceclass" : device.deviceclass,
130 "driver" : device.driver,
131 }
132
133 # PCI devices provide subsystem information, USB don't.
134 if d["subsystem"] == "pci":
135 d["sub_model"] = device.sub_model
136 d["sub_vendor"] = device.sub_vendor
137
138 p["devices"].append(d)
139
140 p["cpu"] = {
141 "arch" : self.arch,
142 "vendor" : self.cpu.vendor,
143 "model" : self.cpu.model,
144 "model_string" : self.cpu.model_string,
145 "stepping" : self.cpu.stepping,
146 "flags" : self.cpu.flags,
147 "speed" : self.cpu.speed,
148 "family" : self.cpu.family,
149 "count" : self.cpu.count
150 }
151
152 if self.cpu.bogomips:
153 p["bogomips"] = self.cpu.bogomips
154
155 p["network"] = {
156 "green" : self.network.has_green(),
157 "blue" : self.network.has_blue(),
158 "orange" : self.network.has_orange(),
159 "red" : self.network.has_red(),
160 }
161
162 # Only append hypervisor information if we are virtualized.
163 if self.virtual:
164 p["hypervisor"] = {
165 "vendor" : self.hypervisor.vendor,
166 }
167
168 return {
169 # Profile version
170 "profile_version" : PROFILE_VERSION,
171
172 # Identification and authorization codes
173 "public_id" : self.public_id,
174 "private_id" : self.private_id,
175
176 # Actual profile data
177 "profile" : p,
178 }
179
180
181 @property
182 def arch(self):
183 return os.uname()[4]
184
185 @property
186 def public_id(self):
187 """
188 This returns a globally (hopefully) ID to identify the host
189 later (by request) in the database.
190 """
191 public_id = self.secret_id
192 if not public_id:
193 return "0" * 40
194
195 return hashlib.sha1(public_id).hexdigest()
196
197 @property
198 def private_id(self):
199 """
200 The private ID is built out of the _unique_id and used to
201 permit a host to do changes on the database.
202
203 No one could ever guess this without access to the host.
204 """
205 private_id = ""
206 for i in reversed(self.secret_id):
207 private_id += i
208
209 if not private_id:
210 return "0" * 40
211
212 return hashlib.sha1(private_id).hexdigest()
213
214 @property
215 def secret_id(self):
216 """
217 Read a "secret" ID from a file if available
218 or calculate it from the hardware.
219 """
220 if os.path.exists(SECRET_ID_FILE):
221 return read_from_file(SECRET_ID_FILE)
222
223 return hashlib.sha1(self._unique_id).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 filter(lambda x: x in string.printable, s)
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(u"\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("/etc/mtab", "r") as f:
405 dev, mountpoint, fs, rest = f.readline().split(" ", 3)
406 if mountpoint == "/" and not fs == "rootfs":
407 # Cut off /dev
408 dev = dev[5:]
409
410 # Handle raids and MMC cards like (mmcblk0p3).
411 if dev[-2] == "p":
412 return dev[:-2]
413
414 # Otherwise cut off all digits at end of string
415 while dev[-1] in string.digits:
416 dev = dev[:-1]
417
418 return dev
419
420 @property
421 def root_size(self):
422 """
423 Return the size of the root disk in kilobytes.
424 """
425 path = "/sys/block/%s/size" % self.root_disk
426 if not os.path.exists(path):
427 return
428
429 with open(path, "r") as f:
430 return int(f.readline()) * 512 / 1024
431
432 @property
433 def root_disk_serial(self):
434 """
435 Return the serial number of the root disk (if any).
436 """
437 try:
438 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
439 except OSError:
440 return
441
442 if serial:
443 # Strip all spaces
444 return serial.strip()
445
446 def scan(self):
447 """
448 Scan for all devices (PCI/USB) in the system and append them
449 to our list.
450 """
451 self.devices = []
452
453 toscan = (
454 ("/sys/bus/pci/devices", device.PCIDevice),
455 ("/sys/bus/usb/devices", device.USBDevice)
456 )
457 for path, cls in toscan:
458 if not os.path.exists(path):
459 continue
460
461 dirlist = os.listdir(path)
462 for dir in dirlist:
463 self.devices.append(cls(os.path.join(path, dir)))
464
465 @property
466 def virtual(self):
467 """
468 Say if the host is running in a virtual environment.
469 """
470 return self.hypervisor.virtual
471
472 @property
473 def network(self):
474 """
475 Reference to the network class.
476 """
477 return network.Network()
478
479
480 if __name__ == "__main__":
481 s=System()
482 print s.arch
483 print s.language
484 print s.release
485 print s.bios_vendor
486 print s.memory
487 print s.kernel
488 print s.root_disk
489 print s.root_size
490 print "------------\n", s.devices, "\n------------\n"
491 print json.dumps(s.profile(), sort_keys=True, indent=4)