]>
Commit | Line | Data |
---|---|---|
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 | 22 | import hashlib |
b45f0e98 MT |
23 | import json |
24 | import os | |
25 | import string | |
26 | ||
3f70e7fd MT |
27 | import _fireinfo |
28 | ||
3b72291d MT |
29 | from . import bios |
30 | from . import cpu | |
31 | from . import device | |
32 | from . import hypervisor | |
33 | from . import network | |
b45f0e98 | 34 | |
0c5ef738 MT |
35 | PROFILE_VERSION = 0 |
36 | ||
45b74ad5 | 37 | SYS_CLASS_DMI = "/sys/class/dmi/id" |
73014efe | 38 | SECRET_ID_FILE = "/etc/fireinfo-id" |
45b74ad5 | 39 | |
7eadbfba MT |
40 | INVALID_ID_STRINGS = ( |
41 | "OEM", "O.E.M.", "o.e.m.", | |
42 | "N/A", "n/a", | |
74ce4c8d | 43 | "12345", "54321", "202020", |
ed491340 | 44 | "Chassis", "chassis", |
d1f67891 | 45 | "Default string", |
80365bb3 | 46 | "EVAL", |
40cffee0 | 47 | "Not Applicable", |
dafc2399 | 48 | "None", "empty", |
edacae4b | 49 | "Serial", "System Serial Number", |
220ffe76 | 50 | "XXXXX", |
40cffee0 | 51 | "01010101-0101-0101-0101-010101010101", |
ac68f90a | 52 | "00020003-0004-0005-0006-000700080009", |
92b4cddf | 53 | "03000200-0400-0500-0006-000700080009", |
80365bb3 | 54 | "11111111-1111-1111-1111-111111111111", |
2689ff71 | 55 | "0000000", "00000000", |
7eadbfba MT |
56 | ) |
57 | ||
220ffe76 MT |
58 | INVALID_ID_STRINGS_EXACT_MATCH = ( |
59 | "NA", | |
60 | ) | |
61 | ||
32aeec73 MT |
62 | class Singleton(type): |
63 | def __init__(cls, name, bases, dict): | |
64 | super(Singleton, cls).__init__(name, bases, dict) | |
65 | cls.instance = None | |
66 | ||
67 | def __call__(cls, *args, **kw): | |
68 | if cls.instance is None: | |
69 | cls.instance = super(Singleton, cls).__call__(*args, **kw) | |
70 | ||
71 | return cls.instance | |
72 | ||
73 | ||
45b74ad5 MT |
74 | def read_from_file(filename): |
75 | """ | |
76 | Read all data from filename. | |
77 | """ | |
78 | if not os.path.exists(filename): | |
79 | return | |
80 | ||
81 | try: | |
82 | with open(filename) as f: | |
83 | return f.read().strip() | |
84 | except IOError: | |
85 | pass | |
86 | ||
3b72291d | 87 | class System(object, metaclass=Singleton): |
b45f0e98 | 88 | def __init__(self): |
e1ca6671 MT |
89 | self.bios = bios.BIOS(self) |
90 | ||
b45f0e98 MT |
91 | # find all devices |
92 | self.devices = [] | |
93 | self.scan() | |
94 | self.cpu = cpu.CPU() | |
715ba5ac MT |
95 | self.hypervisor = hypervisor.Hypervisor() |
96 | ||
3f4e98ab MT |
97 | # Read /proc/cpuinfo for vendor information. |
98 | self.__cpuinfo = self.cpu.read_cpuinfo() | |
99 | ||
b45f0e98 | 100 | def profile(self): |
0c5ef738 MT |
101 | p = {} |
102 | p["system"] = { | |
45b74ad5 MT |
103 | # System information |
104 | "model" : self.model, | |
105 | "vendor" : self.vendor, | |
106 | ||
0c5ef738 MT |
107 | # Indicator if the system is running in a |
108 | # virtual environment. | |
d8614fc3 | 109 | "virtual" : self.virtual, |
b45f0e98 | 110 | |
0c5ef738 MT |
111 | # System language |
112 | "language" : self.language, | |
113 | ||
114 | # Release information | |
115 | "release" : self.release, | |
116 | "kernel_release" : self.kernel_release, | |
117 | ||
118 | "memory" : self.memory, | |
119 | "root_size" : self.root_size, | |
120 | } | |
121 | ||
122 | p["devices"] = [] | |
b45f0e98 | 123 | for device in self.devices: |
97679775 | 124 | d = { |
b45f0e98 MT |
125 | "subsystem" : device.subsystem.lower(), |
126 | "vendor" : device.vendor.lower(), | |
127 | "model" : device.model.lower(), | |
7923840b MT |
128 | "deviceclass" : device.deviceclass, |
129 | "driver" : device.driver, | |
97679775 MT |
130 | } |
131 | ||
132 | # PCI devices provide subsystem information, USB don't. | |
133 | if d["subsystem"] == "pci": | |
134 | d["sub_model"] = device.sub_model | |
135 | d["sub_vendor"] = device.sub_vendor | |
136 | ||
137 | p["devices"].append(d) | |
138 | ||
b45f0e98 | 139 | p["cpu"] = { |
0c5ef738 | 140 | "arch" : self.arch, |
b45f0e98 MT |
141 | "vendor" : self.cpu.vendor, |
142 | "model" : self.cpu.model, | |
eecf2eea | 143 | "model_string" : self.cpu.model_string, |
b45f0e98 MT |
144 | "stepping" : self.cpu.stepping, |
145 | "flags" : self.cpu.flags, | |
b45f0e98 | 146 | "speed" : self.cpu.speed, |
b45f0e98 MT |
147 | "family" : self.cpu.family, |
148 | "count" : self.cpu.count | |
149 | } | |
715ba5ac | 150 | |
a9401d95 MT |
151 | if self.cpu.bogomips: |
152 | p["bogomips"] = self.cpu.bogomips | |
153 | ||
4cb61965 MT |
154 | p["network"] = { |
155 | "green" : self.network.has_green(), | |
156 | "blue" : self.network.has_blue(), | |
157 | "orange" : self.network.has_orange(), | |
158 | "red" : self.network.has_red(), | |
159 | } | |
160 | ||
715ba5ac MT |
161 | # Only append hypervisor information if we are virtualized. |
162 | if self.virtual: | |
163 | p["hypervisor"] = { | |
715ba5ac MT |
164 | "vendor" : self.hypervisor.vendor, |
165 | } | |
166 | ||
0c5ef738 MT |
167 | return { |
168 | # Profile version | |
169 | "profile_version" : PROFILE_VERSION, | |
170 | ||
171 | # Identification and authorization codes | |
172 | "public_id" : self.public_id, | |
173 | "private_id" : self.private_id, | |
174 | ||
175 | # Actual profile data | |
176 | "profile" : p, | |
177 | } | |
b45f0e98 MT |
178 | |
179 | ||
180 | @property | |
181 | def arch(self): | |
182 | return os.uname()[4] | |
183 | ||
184 | @property | |
185 | def public_id(self): | |
31a30328 MT |
186 | """ |
187 | This returns a globally (hopefully) ID to identify the host | |
188 | later (by request) in the database. | |
189 | """ | |
73014efe | 190 | public_id = self.secret_id |
3d10b6d9 MT |
191 | if not public_id: |
192 | return "0" * 40 | |
193 | ||
194 | return hashlib.sha1(public_id).hexdigest() | |
195 | ||
b45f0e98 MT |
196 | @property |
197 | def private_id(self): | |
31a30328 MT |
198 | """ |
199 | The private ID is built out of the _unique_id and used to | |
200 | permit a host to do changes on the database. | |
201 | ||
202 | No one could ever guess this without access to the host. | |
203 | """ | |
5e2ba24e | 204 | private_id = "" |
73014efe | 205 | for i in reversed(self.secret_id): |
5e2ba24e MT |
206 | private_id += i |
207 | ||
3d10b6d9 MT |
208 | if not private_id: |
209 | return "0" * 40 | |
210 | ||
5e2ba24e | 211 | return hashlib.sha1(private_id).hexdigest() |
31a30328 | 212 | |
73014efe MT |
213 | @property |
214 | def secret_id(self): | |
215 | """ | |
216 | Read a "secret" ID from a file if available | |
217 | or calculate it from the hardware. | |
218 | """ | |
219 | if os.path.exists(SECRET_ID_FILE): | |
220 | return read_from_file(SECRET_ID_FILE) | |
221 | ||
222 | return hashlib.sha1(self._unique_id).hexdigest() | |
223 | ||
31a30328 MT |
224 | @property |
225 | def _unique_id(self): | |
226 | """ | |
227 | This is a helper ID which is generated out of some hardware information | |
228 | that is considered to be constant over a PC's lifetime. | |
229 | ||
230 | None of the data here is ever sent to the server. | |
231 | """ | |
ec65e266 | 232 | ids = [] |
3d10b6d9 MT |
233 | |
234 | # Virtual machines (for example) and some boards have a UUID | |
235 | # which is globally unique. | |
236 | for file in ("product_uuid", "product_serial", "chassis_serial"): | |
7eadbfba | 237 | id = read_from_file(os.path.join(SYS_CLASS_DMI, file)) |
c0ef2eb7 | 238 | ids.append(id) |
3d10b6d9 | 239 | |
7eadbfba MT |
240 | # Sort out all bogous or invalid strings from the list. |
241 | _ids = [] | |
242 | for id in ids: | |
88b0ded5 MT |
243 | if id is None: |
244 | continue | |
245 | ||
220ffe76 MT |
246 | for i in INVALID_ID_STRINGS_EXACT_MATCH: |
247 | if id == i: | |
c0ef2eb7 | 248 | id = None |
7eadbfba MT |
249 | break |
250 | ||
220ffe76 MT |
251 | if id: |
252 | for i in INVALID_ID_STRINGS: | |
253 | if i in id: | |
254 | id = None | |
255 | break | |
256 | ||
d58f8ef7 | 257 | # Check if the string only contains 0xff |
deafec98 | 258 | if id and all((e == "\xff" for e in id)): |
d58f8ef7 MT |
259 | id = None |
260 | ||
7eadbfba MT |
261 | if id: |
262 | _ids.append(id) | |
263 | ||
264 | ids = _ids | |
265 | ||
17dc2486 MT |
266 | # Use serial number from root disk (if available) and if |
267 | # no other ID was found, yet. | |
268 | if not ids: | |
269 | root_disk_serial = self.root_disk_serial | |
4ea06541 | 270 | if root_disk_serial and not root_disk_serial.startswith("QM000"): |
17dc2486 | 271 | ids.append(root_disk_serial) |
121f9f20 | 272 | |
f05eac0d MT |
273 | # As last resort, we use the UUID from pakfire. |
274 | if not ids: | |
275 | id = read_from_file("/opt/pakfire/db/uuid") | |
276 | ids.append(id) | |
277 | ||
ec65e266 | 278 | return "#".join(ids) |
31a30328 | 279 | |
b45f0e98 MT |
280 | @property |
281 | def language(self): | |
e8221cde MT |
282 | """ |
283 | Return the language code of IPFire or "unknown" if we cannot get it. | |
284 | """ | |
b3ea53a7 MT |
285 | # Return "unknown" if settings file does not exist. |
286 | filename = "/var/ipfire/main/settings" | |
287 | if not os.path.exists(filename): | |
288 | return "unknown" | |
289 | ||
290 | with open(filename, "r") as f: | |
b45f0e98 MT |
291 | for line in f.readlines(): |
292 | key, val = line.split("=", 1) | |
e8221cde | 293 | if key == "LANGUAGE": |
b45f0e98 MT |
294 | return val.strip() |
295 | ||
296 | @property | |
297 | def release(self): | |
e8221cde MT |
298 | """ |
299 | Return the system release string. | |
300 | """ | |
3f4e98ab | 301 | return read_from_file("/etc/system-release") or "unknown" |
b56bde73 SP |
302 | |
303 | @property | |
304 | def bios_vendor(self): | |
e8221cde MT |
305 | """ |
306 | Return the bios vendor name. | |
307 | """ | |
770c8841 | 308 | return read_from_file("/sys/class/dmi/id/bios_vendor") |
b56bde73 | 309 | |
810fe432 MT |
310 | def vendor_model_tuple(self): |
311 | try: | |
312 | s = self.__cpuinfo["Hardware"] | |
313 | except KeyError: | |
314 | return (None, None) | |
315 | ||
316 | if s.startswith("ARM-Versatile"): | |
317 | return ("ARM", s) | |
318 | ||
319 | try: | |
320 | v, m = s.split(" ", 1) | |
321 | except ValueError: | |
322 | if s.startswith("BCM"): | |
323 | v = "Broadcom" | |
324 | m = s | |
325 | else: | |
326 | v = None | |
327 | m = s | |
328 | ||
329 | return v, m | |
330 | ||
4468fb2e MT |
331 | @staticmethod |
332 | def escape_string(s): | |
333 | """ | |
334 | Will remove all non-printable characters from the given string | |
335 | """ | |
336 | if s is None: | |
337 | return | |
338 | ||
3b72291d | 339 | return [x for x in s if x in string.printable] |
4468fb2e | 340 | |
45b74ad5 MT |
341 | @property |
342 | def vendor(self): | |
e8221cde MT |
343 | """ |
344 | Return the vendor string of this system (if any). | |
345 | """ | |
45b74ad5 | 346 | ret = None |
240fefea | 347 | for file in ("sys_vendor", "board_vendor", "chassis_vendor",): |
45b74ad5 MT |
348 | ret = read_from_file(os.path.join(SYS_CLASS_DMI, file)) |
349 | if ret: | |
4468fb2e | 350 | return self.escape_string(ret) |
45b74ad5 | 351 | |
3383941d MT |
352 | if os.path.exists("/proc/device-tree"): |
353 | ret = self.__cpuinfo.get("Hardware", None) | |
354 | else: | |
355 | ret, m = self.vendor_model_tuple() | |
3f4e98ab | 356 | |
4468fb2e | 357 | return self.escape_string(ret) |
45b74ad5 MT |
358 | |
359 | @property | |
360 | def model(self): | |
e8221cde MT |
361 | """ |
362 | Return the model string of this system (if any). | |
363 | """ | |
45b74ad5 | 364 | ret = None |
84efbcb4 | 365 | for file in ("product_name", "board_model", "chassis_model",): |
45b74ad5 MT |
366 | ret = read_from_file(os.path.join(SYS_CLASS_DMI, file)) |
367 | if ret: | |
4468fb2e | 368 | return self.escape_string(ret) |
45b74ad5 | 369 | |
b9a068e2 MT |
370 | # Read device-tree model if available |
371 | ret = read_from_file("/proc/device-tree/model") | |
372 | if ret: | |
373 | # replace the NULL byte with which the DT string ends | |
3b72291d | 374 | ret = ret.replace("\u0000", "") |
3f4e98ab | 375 | |
810fe432 MT |
376 | # Fall back to read /proc/cpuinfo |
377 | if not ret: | |
378 | v, ret = self.vendor_model_tuple() | |
379 | ||
4468fb2e | 380 | return self.escape_string(ret) |
45b74ad5 | 381 | |
b45f0e98 MT |
382 | @property |
383 | def memory(self): | |
e8221cde MT |
384 | """ |
385 | Return the amount of memory in kilobytes. | |
386 | """ | |
b45f0e98 MT |
387 | with open("/proc/meminfo", "r") as f: |
388 | firstline = f.readline().strip() | |
2ced5413 | 389 | return int(firstline.split()[1]) |
b45f0e98 MT |
390 | |
391 | @property | |
0c5ef738 | 392 | def kernel_release(self): |
e8221cde MT |
393 | """ |
394 | Return the kernel release string. | |
395 | """ | |
b45f0e98 MT |
396 | return os.uname()[2] |
397 | ||
398 | @property | |
399 | def root_disk(self): | |
e8221cde MT |
400 | """ |
401 | Return the dev node of the root disk. | |
402 | """ | |
1565fbf8 MT |
403 | with open("/proc/mounts", "r") as f: |
404 | for line in f.readlines(): | |
405 | # Skip empty lines | |
406 | if not line: | |
407 | continue | |
408 | ||
409 | dev, mountpoint, fs, rest = line.split(" ", 3) | |
410 | if mountpoint == "/" and not fs == "rootfs": | |
411 | # Cut off /dev | |
412 | dev = dev[5:] | |
cf062e0b | 413 | |
1565fbf8 MT |
414 | # Handle raids and MMC cards like (mmcblk0p3). |
415 | if dev[-2] == "p": | |
416 | return dev[:-2] | |
cf062e0b | 417 | |
1565fbf8 MT |
418 | # Otherwise cut off all digits at end of string |
419 | while dev[-1] in string.digits: | |
420 | dev = dev[:-1] | |
e8221cde | 421 | |
1565fbf8 | 422 | return dev |
b45f0e98 MT |
423 | |
424 | @property | |
425 | def root_size(self): | |
e8221cde MT |
426 | """ |
427 | Return the size of the root disk in kilobytes. | |
428 | """ | |
429 | path = "/sys/block/%s/size" % self.root_disk | |
b45f0e98 MT |
430 | if not os.path.exists(path): |
431 | return | |
e8221cde | 432 | |
b45f0e98 | 433 | with open(path, "r") as f: |
e8221cde | 434 | return int(f.readline()) * 512 / 1024 |
3f70e7fd MT |
435 | |
436 | @property | |
437 | def root_disk_serial(self): | |
e8221cde MT |
438 | """ |
439 | Return the serial number of the root disk (if any). | |
440 | """ | |
7890c775 MT |
441 | try: |
442 | serial = _fireinfo.get_harddisk_serial("/dev/%s" % self.root_disk) | |
443 | except OSError: | |
444 | return | |
e658b1a0 MT |
445 | |
446 | if serial: | |
447 | # Strip all spaces | |
448 | return serial.strip() | |
449 | ||
b45f0e98 | 450 | def scan(self): |
e8221cde MT |
451 | """ |
452 | Scan for all devices (PCI/USB) in the system and append them | |
453 | to our list. | |
454 | """ | |
455 | self.devices = [] | |
456 | ||
457 | toscan = ( | |
458 | ("/sys/bus/pci/devices", device.PCIDevice), | |
459 | ("/sys/bus/usb/devices", device.USBDevice) | |
460 | ) | |
b45f0e98 | 461 | for path, cls in toscan: |
94700953 MT |
462 | if not os.path.exists(path): |
463 | continue | |
464 | ||
b45f0e98 MT |
465 | dirlist = os.listdir(path) |
466 | for dir in dirlist: | |
467 | self.devices.append(cls(os.path.join(path, dir))) | |
d8614fc3 MT |
468 | |
469 | @property | |
470 | def virtual(self): | |
471 | """ | |
472 | Say if the host is running in a virtual environment. | |
473 | """ | |
715ba5ac | 474 | return self.hypervisor.virtual |
b45f0e98 | 475 | |
4cb61965 MT |
476 | @property |
477 | def network(self): | |
478 | """ | |
479 | Reference to the network class. | |
480 | """ | |
481 | return network.Network() | |
482 | ||
b45f0e98 MT |
483 | |
484 | if __name__ == "__main__": | |
485 | s=System() | |
3b72291d MT |
486 | print(s.arch) |
487 | print(s.language) | |
488 | print(s.release) | |
489 | print(s.bios_vendor) | |
490 | print(s.memory) | |
491 | print(s.kernel) | |
492 | print(s.root_disk) | |
493 | print(s.root_size) | |
494 | print("------------\n", s.devices, "\n------------\n") | |
495 | print(json.dumps(s.profile(), sort_keys=True, indent=4)) |