]> git.ipfire.org Git - oddments/fireinfo.git/blame - src/fireinfo/system.py
Escape any non-printable ascii characters
[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
7eadbfba
MT
258 if id:
259 _ids.append(id)
260
261 ids = _ids
262
17dc2486
MT
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
4ea06541 267 if root_disk_serial and not root_disk_serial.startswith("QM000"):
17dc2486 268 ids.append(root_disk_serial)
121f9f20 269
f05eac0d
MT
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
ec65e266 275 return "#".join(ids)
31a30328 276
b45f0e98
MT
277 @property
278 def language(self):
e8221cde
MT
279 """
280 Return the language code of IPFire or "unknown" if we cannot get it.
281 """
b3ea53a7
MT
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:
b45f0e98
MT
288 for line in f.readlines():
289 key, val = line.split("=", 1)
e8221cde 290 if key == "LANGUAGE":
b45f0e98
MT
291 return val.strip()
292
293 @property
294 def release(self):
e8221cde
MT
295 """
296 Return the system release string.
297 """
3f4e98ab 298 return read_from_file("/etc/system-release") or "unknown"
b56bde73
SP
299
300 @property
301 def bios_vendor(self):
e8221cde
MT
302 """
303 Return the bios vendor name.
304 """
770c8841 305 return read_from_file("/sys/class/dmi/id/bios_vendor")
b56bde73 306
810fe432
MT
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
4468fb2e
MT
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
45b74ad5
MT
338 @property
339 def vendor(self):
e8221cde
MT
340 """
341 Return the vendor string of this system (if any).
342 """
45b74ad5 343 ret = None
240fefea 344 for file in ("sys_vendor", "board_vendor", "chassis_vendor",):
45b74ad5
MT
345 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
346 if ret:
4468fb2e 347 return self.escape_string(ret)
45b74ad5 348
3383941d
MT
349 if os.path.exists("/proc/device-tree"):
350 ret = self.__cpuinfo.get("Hardware", None)
351 else:
352 ret, m = self.vendor_model_tuple()
3f4e98ab 353
4468fb2e 354 return self.escape_string(ret)
45b74ad5
MT
355
356 @property
357 def model(self):
e8221cde
MT
358 """
359 Return the model string of this system (if any).
360 """
45b74ad5 361 ret = None
84efbcb4 362 for file in ("product_name", "board_model", "chassis_model",):
45b74ad5
MT
363 ret = read_from_file(os.path.join(SYS_CLASS_DMI, file))
364 if ret:
4468fb2e 365 return self.escape_string(ret)
45b74ad5 366
b9a068e2
MT
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", "")
3f4e98ab 372
810fe432
MT
373 # Fall back to read /proc/cpuinfo
374 if not ret:
375 v, ret = self.vendor_model_tuple()
376
4468fb2e 377 return self.escape_string(ret)
45b74ad5 378
b45f0e98
MT
379 @property
380 def memory(self):
e8221cde
MT
381 """
382 Return the amount of memory in kilobytes.
383 """
b45f0e98
MT
384 with open("/proc/meminfo", "r") as f:
385 firstline = f.readline().strip()
2ced5413 386 return int(firstline.split()[1])
b45f0e98
MT
387
388 @property
0c5ef738 389 def kernel_release(self):
e8221cde
MT
390 """
391 Return the kernel release string.
392 """
b45f0e98
MT
393 return os.uname()[2]
394
395 @property
396 def root_disk(self):
e8221cde
MT
397 """
398 Return the dev node of the root disk.
399 """
758f624a 400 with open("/etc/mtab", "r") as f:
ca85ac3a
MT
401 dev, mountpoint, fs, rest = f.readline().split(" ", 3)
402 if mountpoint == "/" and not fs == "rootfs":
e8221cde 403 # Cut off /dev
b45f0e98 404 dev = dev[5:]
cf062e0b
MT
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
b45f0e98 411 while dev[-1] in string.digits:
e8221cde
MT
412 dev = dev[:-1]
413
414 return dev
b45f0e98
MT
415
416 @property
417 def root_size(self):
e8221cde
MT
418 """
419 Return the size of the root disk in kilobytes.
420 """
421 path = "/sys/block/%s/size" % self.root_disk
b45f0e98
MT
422 if not os.path.exists(path):
423 return
e8221cde 424
b45f0e98 425 with open(path, "r") as f:
e8221cde 426 return int(f.readline()) * 512 / 1024
3f70e7fd
MT
427
428 @property
429 def root_disk_serial(self):
e8221cde
MT
430 """
431 Return the serial number of the root disk (if any).
432 """
7890c775
MT
433 try:
434 serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk)
435 except OSError:
436 return
e658b1a0
MT
437
438 if serial:
439 # Strip all spaces
440 return serial.strip()
441
b45f0e98 442 def scan(self):
e8221cde
MT
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 )
b45f0e98 453 for path, cls in toscan:
94700953
MT
454 if not os.path.exists(path):
455 continue
456
b45f0e98
MT
457 dirlist = os.listdir(path)
458 for dir in dirlist:
459 self.devices.append(cls(os.path.join(path, dir)))
d8614fc3
MT
460
461 @property
462 def virtual(self):
463 """
464 Say if the host is running in a virtual environment.
465 """
715ba5ac 466 return self.hypervisor.virtual
b45f0e98 467
4cb61965
MT
468 @property
469 def network(self):
470 """
471 Reference to the network class.
472 """
473 return network.Network()
474
b45f0e98
MT
475
476if __name__ == "__main__":
477 s=System()
478 print s.arch
479 print s.language
480 print s.release
b56bde73 481 print s.bios_vendor
b45f0e98
MT
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"
65891720 487 print json.dumps(s.profile(), sort_keys=True, indent=4)