]> git.ipfire.org Git - oddments/fireinfo.git/blame - src/fireinfo/system.py
Fix crash if there is id has already been reset to None
[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
e1ca6671 29import bios
b45f0e98
MT
30import cpu
31import device
715ba5ac 32import hypervisor
4cb61965 33import 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",
80365bb3 45 "EVAL",
40cffee0 46 "Not Applicable",
dafc2399 47 "None", "empty",
edacae4b 48 "Serial", "System Serial Number",
220ffe76 49 "XXXXX",
40cffee0 50 "01010101-0101-0101-0101-010101010101",
ac68f90a 51 "00020003-0004-0005-0006-000700080009",
92b4cddf 52 "03000200-0400-0500-0006-000700080009",
80365bb3 53 "11111111-1111-1111-1111-111111111111",
2689ff71 54 "0000000", "00000000",
7eadbfba
MT
55)
56
220ffe76
MT
57INVALID_ID_STRINGS_EXACT_MATCH = (
58 "NA",
59)
60
32aeec73
MT
61class 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
45b74ad5
MT
73def 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
b45f0e98 86class System(object):
32aeec73 87 __metaclass__ = Singleton
b45f0e98
MT
88
89 def __init__(self):
e1ca6671
MT
90 self.bios = bios.BIOS(self)
91
b45f0e98
MT
92 # find all devices
93 self.devices = []
94 self.scan()
95 self.cpu = cpu.CPU()
715ba5ac
MT
96 self.hypervisor = hypervisor.Hypervisor()
97
3f4e98ab
MT
98 # Read /proc/cpuinfo for vendor information.
99 self.__cpuinfo = self.cpu.read_cpuinfo()
100
b45f0e98 101 def profile(self):
0c5ef738
MT
102 p = {}
103 p["system"] = {
45b74ad5
MT
104 # System information
105 "model" : self.model,
106 "vendor" : self.vendor,
107
0c5ef738
MT
108 # Indicator if the system is running in a
109 # virtual environment.
d8614fc3 110 "virtual" : self.virtual,
b45f0e98 111
0c5ef738
MT
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"] = []
b45f0e98 124 for device in self.devices:
97679775 125 d = {
b45f0e98
MT
126 "subsystem" : device.subsystem.lower(),
127 "vendor" : device.vendor.lower(),
128 "model" : device.model.lower(),
7923840b
MT
129 "deviceclass" : device.deviceclass,
130 "driver" : device.driver,
97679775
MT
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
b45f0e98 140 p["cpu"] = {
0c5ef738 141 "arch" : self.arch,
b45f0e98
MT
142 "vendor" : self.cpu.vendor,
143 "model" : self.cpu.model,
eecf2eea 144 "model_string" : self.cpu.model_string,
b45f0e98
MT
145 "stepping" : self.cpu.stepping,
146 "flags" : self.cpu.flags,
b45f0e98 147 "speed" : self.cpu.speed,
b45f0e98
MT
148 "family" : self.cpu.family,
149 "count" : self.cpu.count
150 }
715ba5ac 151
a9401d95
MT
152 if self.cpu.bogomips:
153 p["bogomips"] = self.cpu.bogomips
154
4cb61965
MT
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
715ba5ac
MT
162 # Only append hypervisor information if we are virtualized.
163 if self.virtual:
164 p["hypervisor"] = {
715ba5ac
MT
165 "vendor" : self.hypervisor.vendor,
166 }
167
0c5ef738
MT
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 }
b45f0e98
MT
179
180
181 @property
182 def arch(self):
183 return os.uname()[4]
184
185 @property
186 def public_id(self):
31a30328
MT
187 """
188 This returns a globally (hopefully) ID to identify the host
189 later (by request) in the database.
190 """
73014efe 191 public_id = self.secret_id
3d10b6d9
MT
192 if not public_id:
193 return "0" * 40
194
195 return hashlib.sha1(public_id).hexdigest()
196
b45f0e98
MT
197 @property
198 def private_id(self):
31a30328
MT
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 """
5e2ba24e 205 private_id = ""
73014efe 206 for i in reversed(self.secret_id):
5e2ba24e
MT
207 private_id += i
208
3d10b6d9
MT
209 if not private_id:
210 return "0" * 40
211
5e2ba24e 212 return hashlib.sha1(private_id).hexdigest()
31a30328 213
73014efe
MT
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
31a30328
MT
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 """
ec65e266 233 ids = []
3d10b6d9
MT
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"):
7eadbfba 238 id = read_from_file(os.path.join(SYS_CLASS_DMI, file))
c0ef2eb7 239 ids.append(id)
3d10b6d9 240
7eadbfba
MT
241 # Sort out all bogous or invalid strings from the list.
242 _ids = []
243 for id in ids:
88b0ded5
MT
244 if id is None:
245 continue
246
220ffe76
MT
247 for i in INVALID_ID_STRINGS_EXACT_MATCH:
248 if id == i:
c0ef2eb7 249 id = None
7eadbfba
MT
250 break
251
220ffe76
MT
252 if id:
253 for i in INVALID_ID_STRINGS:
254 if i in id:
255 id = None
256 break
257
d58f8ef7 258 # Check if the string only contains 0xff
deafec98 259 if id and all((e == "\xff" for e in id)):
d58f8ef7
MT
260 id = None
261
7eadbfba
MT
262 if id:
263 _ids.append(id)
264
265 ids = _ids
266
17dc2486
MT
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
4ea06541 271 if root_disk_serial and not root_disk_serial.startswith("QM000"):
17dc2486 272 ids.append(root_disk_serial)
121f9f20 273
f05eac0d
MT
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
ec65e266 279 return "#".join(ids)
31a30328 280
b45f0e98
MT
281 @property
282 def language(self):
e8221cde
MT
283 """
284 Return the language code of IPFire or "unknown" if we cannot get it.
285 """
b3ea53a7
MT
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:
b45f0e98
MT
292 for line in f.readlines():
293 key, val = line.split("=", 1)
e8221cde 294 if key == "LANGUAGE":
b45f0e98
MT
295 return val.strip()
296
297 @property
298 def release(self):
e8221cde
MT
299 """
300 Return the system release string.
301 """
3f4e98ab 302 return read_from_file("/etc/system-release") or "unknown"
b56bde73
SP
303
304 @property
305 def bios_vendor(self):
e8221cde
MT
306 """
307 Return the bios vendor name.
308 """
770c8841 309 return read_from_file("/sys/class/dmi/id/bios_vendor")
b56bde73 310
810fe432
MT
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
4468fb2e
MT
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
45b74ad5
MT
342 @property
343 def vendor(self):
e8221cde
MT
344 """
345 Return the vendor string of this system (if any).
346 """
45b74ad5 347 ret = None
240fefea 348 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
45b74ad5
MT
349 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
350 if ret:
4468fb2e 351 return self.escape_string(ret)
45b74ad5 352
3383941d
MT
353 if os.path.exists("/proc/device-tree"):
354 ret = self.__cpuinfo.get("Hardware", None)
355 else:
356 ret, m = self.vendor_model_tuple()
3f4e98ab 357
4468fb2e 358 return self.escape_string(ret)
45b74ad5
MT
359
360 @property
361 def model(self):
e8221cde
MT
362 """
363 Return the model string of this system (if any).
364 """
45b74ad5 365 ret = None
84efbcb4 366 for file in ("product_name", "board_model", "chassis_model",):
45b74ad5
MT
367 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
368 if ret:
4468fb2e 369 return self.escape_string(ret)
45b74ad5 370
b9a068e2
MT
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", "")
3f4e98ab 376
810fe432
MT
377 # Fall back to read /proc/cpuinfo
378 if not ret:
379 v, ret = self.vendor_model_tuple()
380
4468fb2e 381 return self.escape_string(ret)
45b74ad5 382
b45f0e98
MT
383 @property
384 def memory(self):
e8221cde
MT
385 """
386 Return the amount of memory in kilobytes.
387 """
b45f0e98
MT
388 with open("/proc/meminfo", "r") as f:
389 firstline = f.readline().strip()
2ced5413 390 return int(firstline.split()[1])
b45f0e98
MT
391
392 @property
0c5ef738 393 def kernel_release(self):
e8221cde
MT
394 """
395 Return the kernel release string.
396 """
b45f0e98
MT
397 return os.uname()[2]
398
399 @property
400 def root_disk(self):
e8221cde
MT
401 """
402 Return the dev node of the root disk.
403 """
758f624a 404 with open("/etc/mtab", "r") as f:
ca85ac3a
MT
405 dev, mountpoint, fs, rest = f.readline().split(" ", 3)
406 if mountpoint == "/" and not fs == "rootfs":
e8221cde 407 # Cut off /dev
b45f0e98 408 dev = dev[5:]
cf062e0b
MT
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
b45f0e98 415 while dev[-1] in string.digits:
e8221cde
MT
416 dev = dev[:-1]
417
418 return dev
b45f0e98
MT
419
420 @property
421 def root_size(self):
e8221cde
MT
422 """
423 Return the size of the root disk in kilobytes.
424 """
425 path = "/sys/block/%s/size" % self.root_disk
b45f0e98
MT
426 if not os.path.exists(path):
427 return
e8221cde 428
b45f0e98 429 with open(path, "r") as f:
e8221cde 430 return int(f.readline()) * 512 / 1024
3f70e7fd
MT
431
432 @property
433 def root_disk_serial(self):
e8221cde
MT
434 """
435 Return the serial number of the root disk (if any).
436 """
7890c775
MT
437 try:
438 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
439 except OSError:
440 return
e658b1a0
MT
441
442 if serial:
443 # Strip all spaces
444 return serial.strip()
445
b45f0e98 446 def scan(self):
e8221cde
MT
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 )
b45f0e98 457 for path, cls in toscan:
94700953
MT
458 if not os.path.exists(path):
459 continue
460
b45f0e98
MT
461 dirlist = os.listdir(path)
462 for dir in dirlist:
463 self.devices.append(cls(os.path.join(path, dir)))
d8614fc3
MT
464
465 @property
466 def virtual(self):
467 """
468 Say if the host is running in a virtual environment.
469 """
715ba5ac 470 return self.hypervisor.virtual
b45f0e98 471
4cb61965
MT
472 @property
473 def network(self):
474 """
475 Reference to the network class.
476 """
477 return network.Network()
478
b45f0e98
MT
479
480if __name__ == "__main__":
481 s=System()
482 print s.arch
483 print s.language
484 print s.release
b56bde73 485 print s.bios_vendor
b45f0e98
MT
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"
65891720 491 print json.dumps(s.profile(), sort_keys=True, indent=4)