]> git.ipfire.org Git - oddments/fireinfo.git/blob - src/fireinfo/system.py
Escape any non-printable ascii characters
[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 if id:
259 _ids.append(id)
260
261 ids = _ids
262
263 # Use serial number from root disk (if available) and if
264 # no other ID was found, yet.
265 if not ids:
266 root_disk_serial = self.root_disk_serial
267 if root_disk_serial and not root_disk_serial.startswith("QM000"):
268 ids.append(root_disk_serial)
269
270 # As last resort, we use the UUID from pakfire.
271 if not ids:
272 id = read_from_file("/opt/pakfire/db/uuid")
273 ids.append(id)
274
275 return "#".join(ids)
276
277 @property
278 def language(self):
279 """
280 Return the language code of IPFire or "unknown" if we cannot get it.
281 """
282 # Return "unknown" if settings file does not exist.
283 filename = "/var/ipfire/main/settings"
284 if not os.path.exists(filename):
285 return "unknown"
286
287 with open(filename, "r") as f:
288 for line in f.readlines():
289 key, val = line.split("=", 1)
290 if key == "LANGUAGE":
291 return val.strip()
292
293 @property
294 def release(self):
295 """
296 Return the system release string.
297 """
298 return read_from_file("/etc/system-release") or "unknown"
299
300 @property
301 def bios_vendor(self):
302 """
303 Return the bios vendor name.
304 """
305 return read_from_file("/sys/class/dmi/id/bios_vendor")
306
307 def vendor_model_tuple(self):
308 try:
309 s = self.__cpuinfo["Hardware"]
310 except KeyError:
311 return (None, None)
312
313 if s.startswith("ARM-Versatile"):
314 return ("ARM", s)
315
316 try:
317 v, m = s.split(" ", 1)
318 except ValueError:
319 if s.startswith("BCM"):
320 v = "Broadcom"
321 m = s
322 else:
323 v = None
324 m = s
325
326 return v, m
327
328 @staticmethod
329 def escape_string(s):
330 """
331 Will remove all non-printable characters from the given string
332 """
333 if s is None:
334 return
335
336 return filter(lambda x: x in string.printable, s)
337
338 @property
339 def vendor(self):
340 """
341 Return the vendor string of this system (if any).
342 """
343 ret = None
344 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
345 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
346 if ret:
347 return self.escape_string(ret)
348
349 if os.path.exists("/proc/device-tree"):
350 ret = self.__cpuinfo.get("Hardware", None)
351 else:
352 ret, m = self.vendor_model_tuple()
353
354 return self.escape_string(ret)
355
356 @property
357 def model(self):
358 """
359 Return the model string of this system (if any).
360 """
361 ret = None
362 for file in ("product_name", "board_model", "chassis_model",):
363 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
364 if ret:
365 return self.escape_string(ret)
366
367 # Read device-tree model if available
368 ret = read_from_file("/proc/device-tree/model")
369 if ret:
370 # replace the NULL byte with which the DT string ends
371 ret = ret.replace(u"\u0000", "")
372
373 # Fall back to read /proc/cpuinfo
374 if not ret:
375 v, ret = self.vendor_model_tuple()
376
377 return self.escape_string(ret)
378
379 @property
380 def memory(self):
381 """
382 Return the amount of memory in kilobytes.
383 """
384 with open("/proc/meminfo", "r") as f:
385 firstline = f.readline().strip()
386 return int(firstline.split()[1])
387
388 @property
389 def kernel_release(self):
390 """
391 Return the kernel release string.
392 """
393 return os.uname()[2]
394
395 @property
396 def root_disk(self):
397 """
398 Return the dev node of the root disk.
399 """
400 with open("/etc/mtab", "r") as f:
401 dev, mountpoint, fs, rest = f.readline().split(" ", 3)
402 if mountpoint == "/" and not fs == "rootfs":
403 # Cut off /dev
404 dev = dev[5:]
405
406 # Handle raids and MMC cards like (mmcblk0p3).
407 if dev[-2] == "p":
408 return dev[:-2]
409
410 # Otherwise cut off all digits at end of string
411 while dev[-1] in string.digits:
412 dev = dev[:-1]
413
414 return dev
415
416 @property
417 def root_size(self):
418 """
419 Return the size of the root disk in kilobytes.
420 """
421 path = "/sys/block/%s/size" % self.root_disk
422 if not os.path.exists(path):
423 return
424
425 with open(path, "r") as f:
426 return int(f.readline()) * 512 / 1024
427
428 @property
429 def root_disk_serial(self):
430 """
431 Return the serial number of the root disk (if any).
432 """
433 try:
434 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
435 except OSError:
436 return
437
438 if serial:
439 # Strip all spaces
440 return serial.strip()
441
442 def scan(self):
443 """
444 Scan for all devices (PCI/USB) in the system and append them
445 to our list.
446 """
447 self.devices = []
448
449 toscan = (
450 ("/sys/bus/pci/devices", device.PCIDevice),
451 ("/sys/bus/usb/devices", device.USBDevice)
452 )
453 for path, cls in toscan:
454 if not os.path.exists(path):
455 continue
456
457 dirlist = os.listdir(path)
458 for dir in dirlist:
459 self.devices.append(cls(os.path.join(path, dir)))
460
461 @property
462 def virtual(self):
463 """
464 Say if the host is running in a virtual environment.
465 """
466 return self.hypervisor.virtual
467
468 @property
469 def network(self):
470 """
471 Reference to the network class.
472 """
473 return network.Network()
474
475
476 if __name__ == "__main__":
477 s=System()
478 print s.arch
479 print s.language
480 print s.release
481 print s.bios_vendor
482 print s.memory
483 print s.kernel
484 print s.root_disk
485 print s.root_size
486 print "------------\n", s.devices, "\n------------\n"
487 print json.dumps(s.profile(), sort_keys=True, indent=4)