]> git.ipfire.org Git - oddments/fireinfo.git/blob - src/fireinfo/system.py
Exclude some more invalid patterns.
[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 "Not Applicable",
46 "None", "empty",
47 "XXXXX",
48 "01010101-0101-0101-0101-010101010101",
49 "00020003-0004-0005-0006-000700080009",
50 "03000200-0400-0500-0006-000700080009",
51 "0000000", "00000000",
52 )
53
54 INVALID_ID_STRINGS_EXACT_MATCH = (
55 "NA",
56 )
57
58 class Singleton(type):
59 def __init__(cls, name, bases, dict):
60 super(Singleton, cls).__init__(name, bases, dict)
61 cls.instance = None
62
63 def __call__(cls, *args, **kw):
64 if cls.instance is None:
65 cls.instance = super(Singleton, cls).__call__(*args, **kw)
66
67 return cls.instance
68
69
70 def read_from_file(filename):
71 """
72 Read all data from filename.
73 """
74 if not os.path.exists(filename):
75 return
76
77 try:
78 with open(filename) as f:
79 return f.read().strip()
80 except IOError:
81 pass
82
83 class System(object):
84 __metaclass__ = Singleton
85
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 "bogomips" : self.cpu.bogomips,
145 "speed" : self.cpu.speed,
146 "family" : self.cpu.family,
147 "count" : self.cpu.count
148 }
149
150 p["network"] = {
151 "green" : self.network.has_green(),
152 "blue" : self.network.has_blue(),
153 "orange" : self.network.has_orange(),
154 "red" : self.network.has_red(),
155 }
156
157 # Only append hypervisor information if we are virtualized.
158 if self.virtual:
159 p["hypervisor"] = {
160 "vendor" : self.hypervisor.vendor,
161 }
162
163 return {
164 # Profile version
165 "profile_version" : PROFILE_VERSION,
166
167 # Identification and authorization codes
168 "public_id" : self.public_id,
169 "private_id" : self.private_id,
170
171 # Actual profile data
172 "profile" : p,
173 }
174
175
176 @property
177 def arch(self):
178 return os.uname()[4]
179
180 @property
181 def public_id(self):
182 """
183 This returns a globally (hopefully) ID to identify the host
184 later (by request) in the database.
185 """
186 public_id = self.secret_id
187 if not public_id:
188 return "0" * 40
189
190 return hashlib.sha1(public_id).hexdigest()
191
192 @property
193 def private_id(self):
194 """
195 The private ID is built out of the _unique_id and used to
196 permit a host to do changes on the database.
197
198 No one could ever guess this without access to the host.
199 """
200 private_id = ""
201 for i in reversed(self.secret_id):
202 private_id += i
203
204 if not private_id:
205 return "0" * 40
206
207 return hashlib.sha1(private_id).hexdigest()
208
209 @property
210 def secret_id(self):
211 """
212 Read a "secret" ID from a file if available
213 or calculate it from the hardware.
214 """
215 if os.path.exists(SECRET_ID_FILE):
216 return read_from_file(SECRET_ID_FILE)
217
218 return hashlib.sha1(self._unique_id).hexdigest()
219
220 @property
221 def _unique_id(self):
222 """
223 This is a helper ID which is generated out of some hardware information
224 that is considered to be constant over a PC's lifetime.
225
226 None of the data here is ever sent to the server.
227 """
228 ids = []
229
230 # Virtual machines (for example) and some boards have a UUID
231 # which is globally unique.
232 for file in ("product_uuid", "product_serial", "chassis_serial"):
233 id = read_from_file(os.path.join(SYS_CLASS_DMI, file))
234 ids.append(id)
235
236 # Sort out all bogous or invalid strings from the list.
237 _ids = []
238 for id in ids:
239 if id is None:
240 continue
241
242 for i in INVALID_ID_STRINGS_EXACT_MATCH:
243 if id == i:
244 id = None
245 break
246
247 if id:
248 for i in INVALID_ID_STRINGS:
249 if i in id:
250 id = None
251 break
252
253 if id:
254 _ids.append(id)
255
256 ids = _ids
257
258 # Use serial number from root disk (if available) and if
259 # no other ID was found, yet.
260 if not ids:
261 root_disk_serial = self.root_disk_serial
262 if root_disk_serial and not root_disk_serial.startswith("QM000"):
263 ids.append(root_disk_serial)
264
265 # As last resort, we use the UUID from pakfire.
266 if not ids:
267 id = read_from_file("/opt/pakfire/db/uuid")
268 ids.append(id)
269
270 return "#".join(ids)
271
272 @property
273 def language(self):
274 """
275 Return the language code of IPFire or "unknown" if we cannot get it.
276 """
277 # Return "unknown" if settings file does not exist.
278 filename = "/var/ipfire/main/settings"
279 if not os.path.exists(filename):
280 return "unknown"
281
282 with open(filename, "r") as f:
283 for line in f.readlines():
284 key, val = line.split("=", 1)
285 if key == "LANGUAGE":
286 return val.strip()
287
288 @property
289 def release(self):
290 """
291 Return the system release string.
292 """
293 return read_from_file("/etc/system-release") or "unknown"
294
295 @property
296 def bios_vendor(self):
297 """
298 Return the bios vendor name.
299 """
300 return read_from_file("/sys/class/dmi/id/bios_vendor")
301
302 def vendor_model_tuple(self):
303 try:
304 s = self.__cpuinfo["Hardware"]
305 except KeyError:
306 return (None, None)
307
308 if s.startswith("ARM-Versatile"):
309 return ("ARM", s)
310
311 try:
312 v, m = s.split(" ", 1)
313 except ValueError:
314 if s.startswith("BCM"):
315 v = "Broadcom"
316 m = s
317 else:
318 v = None
319 m = s
320
321 return v, m
322
323 @property
324 def vendor(self):
325 """
326 Return the vendor string of this system (if any).
327 """
328 ret = None
329 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
330 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
331 if ret:
332 break
333
334 if ret is None:
335 v, m = self.vendor_model_tuple()
336 ret = v
337
338 return ret
339
340 @property
341 def model(self):
342 """
343 Return the model string of this system (if any).
344 """
345 ret = None
346 for file in ("product_name", "board_model", "chassis_model",):
347 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
348 if ret:
349 break
350
351 if ret is None:
352 v, m = self.vendor_model_tuple()
353 ret = m
354
355 return ret
356
357 @property
358 def memory(self):
359 """
360 Return the amount of memory in kilobytes.
361 """
362 with open("/proc/meminfo", "r") as f:
363 firstline = f.readline().strip()
364 return int(firstline.split()[1])
365
366 @property
367 def kernel_release(self):
368 """
369 Return the kernel release string.
370 """
371 return os.uname()[2]
372
373 @property
374 def root_disk(self):
375 """
376 Return the dev node of the root disk.
377 """
378 with open("/etc/mtab", "r") as f:
379 dev, mountpoint, fs, rest = f.readline().split(" ", 3)
380 if mountpoint == "/" and not fs == "rootfs":
381 # Cut off /dev
382 dev = dev[5:]
383
384 # Handle raids and MMC cards like (mmcblk0p3).
385 if dev[-2] == "p":
386 return dev[:-2]
387
388 # Otherwise cut off all digits at end of string
389 while dev[-1] in string.digits:
390 dev = dev[:-1]
391
392 return dev
393
394 @property
395 def root_size(self):
396 """
397 Return the size of the root disk in kilobytes.
398 """
399 path = "/sys/block/%s/size" % self.root_disk
400 if not os.path.exists(path):
401 return
402
403 with open(path, "r") as f:
404 return int(f.readline()) * 512 / 1024
405
406 @property
407 def root_disk_serial(self):
408 """
409 Return the serial number of the root disk (if any).
410 """
411 try:
412 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
413 except OSError:
414 return
415
416 if serial:
417 # Strip all spaces
418 return serial.strip()
419
420 def scan(self):
421 """
422 Scan for all devices (PCI/USB) in the system and append them
423 to our list.
424 """
425 self.devices = []
426
427 toscan = (
428 ("/sys/bus/pci/devices", device.PCIDevice),
429 ("/sys/bus/usb/devices", device.USBDevice)
430 )
431 for path, cls in toscan:
432 if not os.path.exists(path):
433 continue
434
435 dirlist = os.listdir(path)
436 for dir in dirlist:
437 self.devices.append(cls(os.path.join(path, dir)))
438
439 @property
440 def virtual(self):
441 """
442 Say if the host is running in a virtual environment.
443 """
444 return self.hypervisor.virtual
445
446 @property
447 def network(self):
448 """
449 Reference to the network class.
450 """
451 return network.Network()
452
453
454 if __name__ == "__main__":
455 s=System()
456 print s.arch
457 print s.language
458 print s.release
459 print s.bios_vendor
460 print s.memory
461 print s.kernel
462 print s.root_disk
463 print s.root_size
464 print "------------\n", s.devices, "\n------------\n"
465 print json.dumps(s.profile(), sort_keys=True, indent=4)