]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/fireinfo.py
fireinfo: Allow system/model to be null
[ipfire.org.git] / src / backend / fireinfo.py
1 #!/usr/bin/python
2
3 import datetime
4 import iso3166
5 import json
6 import jsonschema
7 import logging
8 import re
9
10 from . import hwdata
11 from . import util
12 from .misc import Object
13 from .decorators import *
14
15 N_ = lambda x: x
16
17 CPU_VENDORS = {
18 "AMDisbetter!" : "AMD",
19 "AuthenticAMD" : "AMD",
20 "CentaurHauls" : "VIA",
21 "CyrixInstead" : "Cyrix",
22 "GenuineIntel" : "Intel",
23 "TransmetaCPU" : "Transmeta",
24 "GenuineTMx86" : "Transmeta",
25 "Geode by NSC" : "NSC",
26 "NexGenDriven" : "NexGen",
27 "RiseRiseRise" : "Rise",
28 "SiS SiS SiS" : "SiS",
29 "SiS SiS SiS " : "SiS",
30 "UMC UMC UMC " : "UMC",
31 "VIA VIA VIA " : "VIA",
32 "Vortex86 SoC" : "Vortex86",
33 }
34
35 CPU_STRINGS = (
36 ### AMD ###
37 # APU
38 (r"AMD (Sempron)\(tm\) (\d+) APU with Radeon\(tm\) R\d+", r"AMD \1 \2 APU"),
39 (r"AMD ([\w\-]+) APU with Radeon\(tm\) HD Graphics", r"AMD \1 APU"),
40 (r"AMD ([\w\-]+) Radeon R\d+, \d+ Compute Cores \d+C\+\d+G", r"AMD \1 APU"),
41 # Athlon
42 (r"AMD Athlon.* II X2 ([a-z0-9]+).*", r"AMD Athlon X2 \1"),
43 (r"AMD Athlon\(tm\) 64 Processor (\w+)", r"AMD Athlon64 \1"),
44 (r"AMD Athlon\(tm\) 64 X2 Dual Core Processor (\w+)", r"AMD Athlon64 X2 \1"),
45 (r"(AMD Athlon).*(XP).*", r"\1 \2"),
46 (r"(AMD Phenom).* ([0-9]+) .*", r"\1 \2"),
47 (r"(AMD Phenom).*", r"\1"),
48 (r"(AMD Sempron).*", r"\1"),
49 # Geode
50 (r"Geode\(TM\) Integrated Processor by AMD PCS", r"AMD Geode"),
51 (r"(Geode).*", r"\1"),
52 # Mobile
53 (r"Mobile AMD (Athlon|Sempron)\(tm\) Processor (\d+\+?)", r"AMD \1-M \2"),
54
55 # Intel
56 (r"Intel\(R\) (Atom|Celeron).*CPU\s*([A-Z0-9]+) .*", r"Intel \1 \2"),
57 (r"(Intel).*(Celeron).*", r"\1 \2"),
58 (r"Intel\(R\)? Core\(TM\)?2 Duo *CPU .* ([A-Z0-9]+) .*", r"Intel C2D \1"),
59 (r"Intel\(R\)? Core\(TM\)?2 Duo CPU (\w+)", r"Intel C2D \1"),
60 (r"Intel\(R\)? Core\(TM\)?2 CPU .* ([A-Z0-9]+) .*", r"Intel C2 \1"),
61 (r"Intel\(R\)? Core\(TM\)?2 Quad *CPU .* ([A-Z0-9]+) .*", r"Intel C2Q \1"),
62 (r"Intel\(R\)? Core\(TM\)? (i[753]\-\w+) CPU", r"Intel Core \1"),
63 (r"Intel\(R\)? Xeon\(R\)? CPU (\w+) (0|v\d+)", r"Intel Xeon \1 \2"),
64 (r"Intel\(R\)? Xeon\(R\)? CPU\s+(\w+)", r"Intel Xeon \1"),
65 (r"(Intel).*(Xeon).*", r"\1 \2"),
66 (r"Intel.* Pentium.* (D|4) .*", r"Intel Pentium \1"),
67 (r"Intel.* Pentium.* Dual .* ([A-Z0-9]+) .*", r"Intel Pentium Dual \1"),
68 (r"Pentium.* Dual-Core .* ([A-Z0-9]+) .*", r"Intel Pentium Dual \1"),
69 (r"(Pentium I{2,3}).*", r"Intel \1"),
70 (r"(Celeron \(Coppermine\))", r"Intel Celeron"),
71
72 # NSC
73 (r"Geode\(TM\) Integrated Processor by National Semi", r"NSC Geode"),
74
75 # VIA
76 (r"(VIA \w*).*", r"\1"),
77
78 # Qemu
79 (r"QEMU Virtual CPU version .*", r"QEMU CPU"),
80
81 # ARM
82 (r"Feroceon .*", r"ARM Feroceon"),
83 )
84
85 PROFILE_SCHEMA = {
86 "$schema" : "https://json-schema.org/draft/2020-12/schema",
87 "$id" : "https://fireinfo.ipfire.org/profile.schema.json",
88 "title" : "Fireinfo Profile",
89 "description" : "Fireinfo Profile",
90 "type" : "object",
91
92 # Properties
93 "properties" : {
94 # Processor
95 "cpu" : {
96 "type" : "object",
97 "properties" : {
98 "arch" : {
99 "type" : "string",
100 "pattern" : r"^[a-z0-9\_]{,8}$",
101 },
102 "count" : {
103 "type" : "integer",
104 },
105 "family" : {
106 "type" : ["integer", "null"],
107 },
108 "flags" : {
109 "type" : "array",
110 "items" : {
111 "type" : "string",
112 "pattern" : r"^.{,24}$",
113 },
114 },
115 "model" : {
116 "type" : "integer",
117 },
118 "model_string" : {
119 "type" : ["string", "null"],
120 "pattern" : r"^.{,80}$",
121 },
122 "speed" : {
123 "type" : "number",
124 },
125 "stepping" : {
126 "type" : "integer",
127 },
128 "vendor" : {
129 "type" : "string",
130 "pattern" : r"^.{,80}$",
131 },
132 },
133 "additionalProperties" : False,
134 "required" : [
135 "arch",
136 "count",
137 "family",
138 "flags",
139 "model",
140 "model_string",
141 "speed",
142 "stepping",
143 "vendor",
144 ],
145 },
146
147 # Devices
148 "devices" : {
149 "type" : "array",
150 "items" : {
151 "type" : "object",
152 "properties" : {
153 "deviceclass" : {
154 "type" : ["string", "null"],
155 "pattern" : r"^.{,20}$",
156 },
157 "driver" : {
158 "type" : ["string", "null"],
159 "pattern" : r"^.{,24}$",
160 },
161 "model" : {
162 "type" : "string",
163 "pattern" : r"^[a-z0-9]{4}$",
164 },
165 "sub_model" : {
166 "type" : ["string", "null"],
167 "pattern" : r"^[a-z0-9]{4}$",
168 },
169 "sub_vendor" : {
170 "type" : ["string", "null"],
171 "pattern" : r"^[a-z0-9]{4}$",
172 },
173 "subsystem" : {
174 "type" : "string",
175 "pattern" : r"^[a-z]{3}$",
176 },
177 "vendor" : {
178 "type" : "string",
179 "pattern" : r"^[a-z0-9]{4}$",
180 },
181 },
182 "additionalProperties" : False,
183 "required" : [
184 "deviceclass",
185 "driver",
186 "model",
187 "subsystem",
188 "vendor",
189 ],
190 },
191 },
192
193 # Network
194 "network" : {
195 "type" : "object",
196 "properties" : {
197 "blue" : {
198 "type" : "boolean",
199 },
200 "green" : {
201 "type" : "boolean",
202 },
203 "orange" : {
204 "type" : "boolean",
205 },
206 "red" : {
207 "type" : "boolean",
208 },
209 },
210 "additionalProperties" : False,
211 },
212
213 # System
214 "system" : {
215 "type" : "object",
216 "properties" : {
217 "kernel_release" : {
218 "type" : "string",
219 "pattern" : r"^.{,40}$",
220 },
221 "language" : {
222 "type" : "string",
223 "pattern" : r"^[a-z]{2}(\.utf8)?$",
224 },
225 "memory" : {
226 "type" : "integer",
227 },
228 "model" : {
229 "type" : ["string", "null"],
230 "pattern" : r"^.{,80}$",
231 },
232 "release" : {
233 "type" : "string",
234 "pattern" : r"^.{,80}$",
235 },
236 "root_size" : {
237 "type" : "number",
238 },
239 "vendor" : {
240 "type" : "string",
241 "pattern" : r"^.{,80}$",
242 },
243 "virtual" : {
244 "type" : "boolean"
245 },
246 },
247 "additionalProperties" : False,
248 "required" : [
249 "kernel_release",
250 "language",
251 "memory",
252 "model",
253 "release",
254 "root_size",
255 "vendor",
256 "virtual",
257 ],
258 },
259
260 # Hypervisor
261 "hypervisor" : {
262 "type" : "object",
263 "properties" : {
264 "vendor" : {
265 "type" : "string",
266 "pattern" : r"^.{,40}$",
267 },
268 },
269 "additionalProperties" : False,
270 "required" : [
271 "vendor",
272 ],
273 },
274
275 # Error - BogoMIPS
276 "bogomips" : {
277 "type" : "number",
278 },
279 },
280 "additionalProperties" : False,
281 "required" : [
282 "cpu",
283 "devices",
284 "network",
285 "system",
286 ],
287 }
288
289 class Network(Object):
290 def init(self, blob):
291 self.blob = blob
292
293 def __iter__(self):
294 ret = []
295
296 for zone in ("red", "green", "orange", "blue"):
297 if self.has_zone(zone):
298 ret.append(zone)
299
300 return iter(ret)
301
302 def has_zone(self, name):
303 return self.blob.get(name, False)
304
305 @property
306 def has_red(self):
307 return self.has_zone("red")
308
309 @property
310 def has_green(self):
311 return self.has_zone("green")
312
313 @property
314 def has_orange(self):
315 return self.has_zone("orange")
316
317 @property
318 def has_blue(self):
319 return self.has_zone("blue")
320
321
322 class Processor(Object):
323 def init(self, blob):
324 self.blob = blob
325
326 def __str__(self):
327 s = []
328
329 if self.model_string and not self.model_string.startswith(self.vendor):
330 s.append(self.vendor)
331 s.append("-")
332
333 s.append(self.model_string or "Generic")
334
335 if self.core_count > 1:
336 s.append("x%s" % self.core_count)
337
338 return " ".join(s)
339
340 @property
341 def vendor(self):
342 vendor = self.blob.get("vendor")
343
344 try:
345 return CPU_VENDORS[vendor]
346 except KeyError:
347 return vendor
348
349 @property
350 def family(self):
351 return self.blob.get("family")
352
353 @property
354 def model(self):
355 return self.blob.get("model")
356
357 @property
358 def stepping(self):
359 return self.blob.get("stepping")
360
361 @property
362 def model_string(self):
363 return self.blob.get("model_string")
364
365 @property
366 def flags(self):
367 return self.blob.get("flags")
368
369 def has_flag(self, flag):
370 return flag in self.flags
371
372 def uses_ht(self):
373 if self.vendor == "Intel" and self.family == 6 and self.model in (15, 55, 76, 77):
374 return False
375
376 return self.has_flag("ht")
377
378 @property
379 def core_count(self):
380 return self.blob.get("count", 1)
381
382 @property
383 def count(self):
384 if self.uses_ht():
385 return self.core_count // 2
386
387 return self.core_count
388
389 @property
390 def clock_speed(self):
391 return self.__clock_speed
392
393 def format_clock_speed(self):
394 if not self.clock_speed:
395 return
396
397 if self.clock_speed < 1000:
398 return "%dMHz" % self.clock_speed
399
400 return "%.2fGHz" % round(self.clock_speed / 1000, 2)
401
402 @property
403 def bogomips(self):
404 return self.__bogomips
405
406 @property
407 def capabilities(self):
408 caps = [
409 ("64bit", self.has_flag("lm")),
410 ("aes", self.has_flag("aes")),
411 ("nx", self.has_flag("nx")),
412 ("pae", self.has_flag("pae") or self.has_flag("lpae")),
413 ("rdrand", self.has_flag("rdrand")),
414 ]
415
416 # If the system is already running in a virtual environment,
417 # we cannot correctly detect if the CPU supports svm or vmx
418 if self.has_flag("hypervisor"):
419 caps.append(("virt", None))
420 else:
421 caps.append(("virt", self.has_flag("vmx") or self.has_flag("svm")))
422
423 return caps
424
425 def format_model(self):
426 s = self.model_string or ""
427
428 # Remove everything after the @: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
429 s, sep, rest = s.partition("@")
430
431 for pattern, repl in CPU_STRINGS:
432 if re.match(pattern, s) is None:
433 continue
434
435 s = re.sub(pattern, repl, s)
436 break
437
438 # Otherwise remove the symbols
439 for i in ("C", "R", "TM", "tm"):
440 s = s.replace("(%s)" % i, "")
441
442 # Replace too long strings with shorter ones
443 pairs = (
444 ("Quad-Core Processor", ""),
445 ("Dual-Core Processor", ""),
446 ("Processor", "CPU"),
447 ("processor", "CPU"),
448 )
449 for k, v in pairs:
450 s = s.replace(k, v)
451
452 # Remove too many spaces
453 s = " ".join((e for e in s.split() if e))
454
455 return s
456
457 @property
458 def friendly_string(self):
459 s = []
460
461 model = self.format_model()
462 if model:
463 s.append(model)
464
465 clock_speed = self.format_clock_speed()
466 if clock_speed:
467 s.append("@ %s" % clock_speed)
468
469 if self.count > 1:
470 s.append("x%s" % self.count)
471
472 return " ".join(s)
473
474
475 class Device(Object):
476 classid2name = {
477 "pci" : {
478 "00" : N_("Unclassified"),
479 "01" : N_("Mass storage"),
480 "02" : N_("Network"),
481 "03" : N_("Display"),
482 "04" : N_("Multimedia"),
483 "05" : N_("Memory controller"),
484 "06" : N_("Bridge"),
485 "07" : N_("Communication"),
486 "08" : N_("Generic system peripheral"),
487 "09" : N_("Input device"),
488 "0a" : N_("Docking station"),
489 "0b" : N_("Processor"),
490 "0c" : N_("Serial bus"),
491 "0d" : N_("Wireless"),
492 "0e" : N_("Intelligent controller"),
493 "0f" : N_("Satellite communications controller"),
494 "10" : N_("Encryption"),
495 "11" : N_("Signal processing controller"),
496 "ff" : N_("Unassigned class"),
497 },
498
499 "usb" : {
500 "00" : N_("Unclassified"),
501 "01" : N_("Multimedia"),
502 "02" : N_("Communication"),
503 "03" : N_("Input device"),
504 "05" : N_("Generic system peripheral"),
505 "06" : N_("Image"),
506 "07" : N_("Printer"),
507 "08" : N_("Mass storage"),
508 "09" : N_("Hub"),
509 "0a" : N_("Communication"),
510 "0b" : N_("Smart card"),
511 "0d" : N_("Encryption"),
512 "0e" : N_("Display"),
513 "0f" : N_("Personal Healthcare"),
514 "dc" : N_("Diagnostic Device"),
515 "e0" : N_("Wireless"),
516 "ef" : N_("Unclassified"),
517 "fe" : N_("Unclassified"),
518 "ff" : N_("Unclassified"),
519 }
520 }
521
522 def init(self, blob):
523 self.blob = blob
524
525 def __repr__(self):
526 return "<%s vendor=%s model=%s>" % (self.__class__.__name__,
527 self.vendor_string, self.model_string)
528
529 def __eq__(self, other):
530 if isinstance(other, self.__class__):
531 return self.blob == other.blob
532
533 return NotImplemented
534
535 def __lt__(self, other):
536 if isinstance(other, self.__class__):
537 return self.cls < other.cls or \
538 self.vendor_string < other.vendor_string or \
539 self.vendor < other.vendor or \
540 self.model_string < other.model_string or \
541 self.model < other.model
542
543 return NotImplemented
544
545 def is_showable(self):
546 if self.driver in ("usb", "pcieport", "hub"):
547 return False
548
549 return True
550
551 @property
552 def subsystem(self):
553 return self.blob.get("subsystem")
554
555 @property
556 def model(self):
557 return self.blob.get("model")
558
559 @lazy_property
560 def model_string(self):
561 return self.fireinfo.get_model_string(self.subsystem, self.vendor, self.model)
562
563 @property
564 def vendor(self):
565 return self.blob.get("vendor")
566
567 @lazy_property
568 def vendor_string(self):
569 return self.fireinfo.get_vendor_string(self.subsystem, self.vendor)
570
571 @property
572 def driver(self):
573 return self.blob.get("driver")
574
575 @lazy_property
576 def cls(self):
577 classid = self.blob.get("deviceclass")
578
579 if self.subsystem == "pci":
580 classid = classid[:-4]
581 if len(classid) == 1:
582 classid = "0%s" % classid
583
584 elif self.subsystem == "usb" and classid:
585 classid = classid.split("/")[0]
586 classid = "%02x" % int(classid)
587
588 try:
589 return self.classid2name[self.subsystem][classid]
590 except KeyError:
591 return "N/A"
592
593
594 class System(Object):
595 def init(self, blob):
596 self.blob = blob
597
598 @property
599 def arch(self):
600 return self.blob.get("arch")
601
602 @property
603 def language(self):
604 return self.blob.get("language")
605
606 @property
607 def vendor(self):
608 return self.blob.get("vendor")
609
610 @property
611 def model(self):
612 return self.blob.get("model")
613
614 @property
615 def release(self):
616 return self.blob.get("release")
617
618 @property
619 def storage(self):
620 return self.blob.get("storage_size", 0)
621
622 def is_virtual(self):
623 return self.blob.get("virtual", False)
624
625
626 class Hypervisor(Object):
627 def init(self, blob):
628 self.blob = blob
629
630 def __str__(self):
631 return self.vendor
632
633 @property
634 def vendor(self):
635 return self.blob.get("vendor")
636
637
638 class Profile(Object):
639 def init(self, profile_id, private_id, created_at, expired_at, version, blob,
640 last_updated_at, country_code, **kwargs):
641 self.profile_id = profile_id
642 self.private_id = private_id
643 self.created_at = created_at
644 self.expired_at = expired_at
645 self.version = version
646 self.blob = blob
647 self.last_updated_at = last_updated_at
648 self.country_code = country_code
649
650 def __repr__(self):
651 return "<%s %s>" % (self.__class__.__name__, self.profile_id)
652
653 def is_showable(self):
654 return True if self.blob else False
655
656 @property
657 def public_id(self):
658 """
659 An alias for the profile ID
660 """
661 return self.profile_id
662
663 # Location
664
665 @property
666 def location(self):
667 return self.country_code
668
669 @property
670 def location_string(self):
671 return self.backend.get_country_name(self.location) or self.location
672
673 # Devices
674
675 @lazy_property
676 def devices(self):
677 return [Device(self.backend, blob) for blob in self.blob.get("devices", [])]
678
679 # System
680
681 @lazy_property
682 def system(self):
683 return System(self.backend, self.blob.get("system", {}))
684
685 # Processor
686
687 @property
688 def processor(self):
689 return Processor(self.backend, self.blob.get("cpu", {}))
690
691 # Memory
692
693 @property
694 def memory(self):
695 return self.blob.get("memory")
696
697 @property
698 def friendly_memory(self):
699 return util.format_size(self.memory or 0)
700
701 # Virtual
702
703 def is_virtual(self):
704 return self.system.is_virtual()
705
706 @property
707 def hypervisor(self):
708 return Hypervisor(self.backend, self.blob.get("hypervisor"))
709
710 # Network
711
712 @lazy_property
713 def network(self):
714 return Network(self.backend, self.blob.get("network", {}))
715
716
717 class Fireinfo(Object):
718 async def expire(self):
719 """
720 Called to expire any profiles that have not been updated in a fortnight
721 """
722 self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
723 WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime.timedelta(days=14))
724
725 def _get_profile(self, query, *args, **kwargs):
726 res = self.db.get(query, *args, **kwargs)
727
728 if res:
729 return Profile(self.backend, **res)
730
731 def get_profile_count(self, when=None):
732 if when:
733 res = self.db.get("""
734 SELECT
735 COUNT(*) AS count
736 FROM
737 fireinfo
738 WHERE
739 created_at <= %s
740 AND
741 (
742 expired_at IS NULL
743 OR
744 expired_at > %s
745 )
746 """)
747 else:
748 res = self.db.get("""
749 SELECT
750 COUNT(*) AS count
751 FROM
752 fireinfo
753 WHERE
754 expired_at IS NULL
755 """,
756 )
757
758 return res.count if res else 0
759
760 def get_profile_histogram(self):
761 today = datetime.date.today()
762
763 t1 = datetime.date(year=today.year - 10, month=today.month, day=1)
764 t2 = datetime.date(year=today.year, month=today.month, day=1)
765
766 res = self.db.query("""
767 SELECT
768 date,
769 COUNT(*) AS count
770 FROM
771 generate_series(%s, %s, INTERVAL '1 month') date
772 JOIN
773 fireinfo ON date >= created_at
774 AND (expired_at IS NULL OR expired_at > date)
775 GROUP BY
776 date
777 """, t1, t2)
778
779 return { row.date : row.count for row in res }
780
781 # Profiles
782
783 def get_profile(self, profile_id, when=None):
784 if when:
785 return self._get_profile("""
786 SELECT
787 *
788 FROM
789 fireinfo
790 WHERE
791 profile_id = %s
792 AND
793 %s BETWEEN created_at AND expired_at
794 """, profile_id,
795 )
796
797 return self._get_profile("""
798 SELECT
799 *
800 FROM
801 fireinfo
802 WHERE
803 profile_id = %s
804 AND
805 expired_at IS NULL
806 """, profile_id,
807 )
808
809 # Handle profile
810
811 def handle_profile(self, profile_id, blob, country_code=None, asn=None, when=None):
812 private_id = blob.get("private_id", None)
813 assert private_id
814
815 now = datetime.datetime.utcnow()
816
817 # Fetch the profile version
818 version = blob.get("profile_version")
819
820 # Extract the profile
821 profile = blob.get("profile")
822
823 if profile:
824 # Validate the profile
825 self._validate(profile_id, version, profile)
826
827 # Pre-process the profile
828 profile = self._preprocess(profile)
829
830 # Fetch the previous profile
831 prev = self.get_profile(profile_id)
832
833 if prev:
834 # Check if the private ID matches
835 if not prev.private_id == private_id:
836 logging.error("Private ID for profile %s does not match" % profile_id)
837 return False
838
839 # Check when the last update was
840 elif now - prev.last_updated_at < datetime.timedelta(hours=6):
841 logging.warning("Profile %s has been updated too soon" % profile_id)
842 return False
843
844 # Check if the profile has changed
845 elif prev.version == version and prev.blob == blob:
846 logging.debug("Profile %s has not changed" % profile_id)
847
848 # Update the timestamp
849 self.db.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
850 WHERE profile_id = %s AND expired_at IS NULL", profile_id)
851
852 return True
853
854 # Delete the previous profile
855 self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
856 WHERE profile_id = %s AND expired_at IS NULL", profile_id)
857
858 # Store the new profile
859 self.db.execute("""
860 INSERT INTO
861 fireinfo
862 (
863 profile_id,
864 private_id,
865 version,
866 blob,
867 country_code,
868 asn
869 )
870 VALUES
871 (
872 %s,
873 %s,
874 %s,
875 %s,
876 %s,
877 %s
878 )
879 """, profile_id, private_id, version, json.dumps(profile), country_code, asn,
880 )
881
882 def _validate(self, profile_id, version, blob):
883 """
884 Validate the profile
885 """
886 if not version == 0:
887 raise ValueError("Unsupported profile version")
888
889 # Validate the blob
890 try:
891 return jsonschema.validate(blob, schema=PROFILE_SCHEMA)
892
893 # Raise a ValueError instead which is easier to handle later on
894 except jsonschema.exceptions.ValidationError as e:
895 raise ValueError("%s" % e) from e
896
897 def _preprocess(self, blob):
898 """
899 Modifies the profile before storing it
900 """
901 # Remove the architecture from the release string
902 blob["system"]["release"]= self._filter_release(blob["system"]["release"])
903
904 return blob
905
906 def _filter_release(self, release):
907 """
908 Removes the arch part
909 """
910 r = [e for e in release.split() if e]
911
912 for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
913 try:
914 r.remove(s)
915 break
916 except ValueError:
917 pass
918
919 return " ".join(r)
920
921 # Data outputs
922
923 def get_random_profile(self, when=None):
924 if when:
925 return self._get_profile("""
926 SELECT
927 *
928 FROM
929 fireinfo
930 WHERE
931 created_at <= %s
932 AND
933 (
934 expired_at IS NULL
935 OR
936 expired_at > %s
937 )
938 ORDER BY
939 RANDOM()
940 LIMIT
941 1
942 """, when, when,
943 )
944
945 return self._get_profile("""
946 SELECT
947 *
948 FROM
949 fireinfo
950 WHERE
951 expired_at IS NULL
952 ORDER BY
953 RANDOM()
954 LIMIT
955 1
956 """)
957
958 def get_active_profiles(self, when=None):
959 if when:
960 raise NotImplementedError
961
962 else:
963 res = self.db.get("""
964 SELECT
965 COUNT(*) AS total_profiles,
966 COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
967 FROM
968 fireinfo
969 WHERE
970 expired_at IS NULL
971 """)
972
973 if res:
974 return res.active_profiles, res.total_profiles
975
976 def get_geo_location_map(self, when=None):
977 if when:
978 res = self.db.query("""
979 SELECT
980 country_code,
981 fireinfo_percentage(
982 COUNT(*), SUM(COUNT(*)) OVER ()
983 ) AS p
984 FROM
985 fireinfo
986 WHERE
987 created_at <= %s
988 AND
989 (
990 expired_at IS NULL
991 OR
992 expired_at > %s
993 )
994 AND
995 country_code IS NOT NULL
996 GROUP BY
997 country_code
998 """, when, when)
999 else:
1000 res = self.db.query("""
1001 SELECT
1002 country_code,
1003 fireinfo_percentage(
1004 COUNT(*), SUM(COUNT(*)) OVER ()
1005 ) AS p
1006 FROM
1007 fireinfo
1008 WHERE
1009 expired_at IS NULL
1010 AND
1011 country_code IS NOT NULL
1012 GROUP BY
1013 country_code
1014 """)
1015
1016 return { row.country_code : row.p for row in res }
1017
1018 def get_asn_map(self, when=None):
1019 if when:
1020 res = self.db.query("""
1021 SELECT
1022 asn,
1023 fireinfo_percentage(
1024 COUNT(*), SUM(COUNT(*)) OVER ()
1025 ) AS p,
1026 COUNT(*) AS c
1027 FROM
1028 fireinfo
1029 WHERE
1030 created_at <= %s
1031 AND
1032 (
1033 expired_at IS NULL
1034 OR
1035 expired_at > %s
1036 )
1037 AND
1038 asn IS NOT NULL
1039 GROUP BY
1040 asn
1041 """, when, when)
1042 else:
1043 res = self.db.query("""
1044 SELECT
1045 asn,
1046 fireinfo_percentage(
1047 COUNT(*), SUM(COUNT(*)) OVER ()
1048 ) AS p,
1049 COUNT(*) AS c
1050 FROM
1051 fireinfo
1052 WHERE
1053 expired_at IS NULL
1054 AND
1055 asn IS NOT NULL
1056 GROUP BY
1057 asn
1058 """)
1059
1060 return { self.backend.location.get_as(row.asn) : (row.c, row.p) for row in res }
1061
1062 @property
1063 def cpu_vendors(self):
1064 res = self.db.query("""
1065 SELECT DISTINCT
1066 blob->'cpu'->'vendor' AS vendor
1067 FROM
1068 fireinfo
1069 WHERE
1070 blob->'cpu'->'vendor' IS NOT NULL
1071 """,
1072 )
1073
1074 return sorted((CPU_VENDORS.get(row.vendor, row.vendor) for row in res))
1075
1076 def get_cpu_vendors_map(self, when=None):
1077 if when:
1078 raise NotImplementedError
1079
1080 else:
1081 res = self.db.query("""
1082 SELECT
1083 blob->'cpu'->'vendor' AS vendor,
1084 fireinfo_percentage(
1085 COUNT(*), SUM(COUNT(*)) OVER ()
1086 ) AS p
1087 FROM
1088 fireinfo
1089 WHERE
1090 expired_at IS NULL
1091 AND
1092 blob IS NOT NULL
1093 AND
1094 blob->'cpu'->'vendor' IS NOT NULL
1095 GROUP BY
1096 blob->'cpu'->'vendor'
1097 """)
1098
1099 return { CPU_VENDORS.get(row.vendor, row.vendor) : row.p for row in res }
1100
1101 def get_cpu_flags_map(self, when=None):
1102 if when:
1103 raise NotImplementedError
1104
1105 else:
1106 res = self.db.query("""
1107 WITH arch_flags AS (
1108 SELECT
1109 ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
1110 blob->'cpu'->'arch' AS arch,
1111 blob->'cpu'->'flags' AS flags
1112 FROM
1113 fireinfo
1114 WHERE
1115 expired_at IS NULL
1116 AND
1117 blob->'cpu'->'arch' IS NOT NULL
1118 AND
1119 blob->'cpu'->'flags' IS NOT NULL
1120
1121 -- Filter out virtual systems
1122 AND
1123 CAST((blob->'system'->'virtual') AS boolean) IS FALSE
1124 )
1125
1126 SELECT
1127 arch,
1128 flag,
1129 fireinfo_percentage(
1130 COUNT(*),
1131 (
1132 SELECT
1133 MAX(id)
1134 FROM
1135 arch_flags __arch_flags
1136 WHERE
1137 arch_flags.arch = __arch_flags.arch
1138 )
1139 ) AS p
1140 FROM
1141 arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
1142 GROUP BY
1143 arch, flag
1144 """)
1145
1146 result = {}
1147
1148 for row in res:
1149 try:
1150 result[row.arch][row.flag] = row.p
1151 except KeyError:
1152 result[row.arch] = { row.flag : row.p }
1153
1154 return result
1155
1156 def get_average_memory_amount(self, when=None):
1157 if when:
1158 res = self.db.get("""
1159 SELECT
1160 AVG(
1161 CAST(blob->'system'->'memory' AS numeric)
1162 ) AS memory
1163 FROM
1164 fireinfo
1165 WHERE
1166 created_at <= %s
1167 AND
1168 (
1169 expired_at IS NULL
1170 OR
1171 expired_at > %s
1172 )
1173 """, when)
1174 else:
1175 res = self.db.get("""
1176 SELECT
1177 AVG(
1178 CAST(blob->'system'->'memory' AS numeric)
1179 ) AS memory
1180 FROM
1181 fireinfo
1182 WHERE
1183 expired_at IS NULL
1184 """,)
1185
1186 return res.memory if res else 0
1187
1188 def get_arch_map(self, when=None):
1189 if when:
1190 raise NotImplementedError
1191
1192 else:
1193 res = self.db.query("""
1194 SELECT
1195 blob->'cpu'->'arch' AS arch,
1196 fireinfo_percentage(
1197 COUNT(*), SUM(COUNT(*)) OVER ()
1198 ) AS p
1199 FROM
1200 fireinfo
1201 WHERE
1202 expired_at IS NULL
1203 AND
1204 blob->'cpu'->'arch' IS NOT NULL
1205 GROUP BY
1206 blob->'cpu'->'arch'
1207 """)
1208
1209 return { row.arch : row.p for row in res }
1210
1211 # Virtual
1212
1213 def get_hypervisor_map(self, when=None):
1214 if when:
1215 raise NotImplementedError
1216 else:
1217 res = self.db.query("""
1218 SELECT
1219 blob->'hypervisor'->'vendor' AS vendor,
1220 fireinfo_percentage(
1221 COUNT(*), SUM(COUNT(*)) OVER ()
1222 ) AS p
1223 FROM
1224 fireinfo
1225 WHERE
1226 expired_at IS NULL
1227 AND
1228 CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1229 AND
1230 blob->'hypervisor'->'vendor' IS NOT NULL
1231 GROUP BY
1232 blob->'hypervisor'->'vendor'
1233 """)
1234
1235 return { row.vendor : row.p for row in res }
1236
1237 def get_virtual_ratio(self, when=None):
1238 if when:
1239 raise NotImplementedError
1240
1241 else:
1242 res = self.db.get("""
1243 SELECT
1244 fireinfo_percentage(
1245 COUNT(*) FILTER (
1246 WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1247 ),
1248 COUNT(*)
1249 ) AS p
1250 FROM
1251 fireinfo
1252 WHERE
1253 expired_at IS NULL
1254 AND
1255 blob IS NOT NULL
1256 """)
1257
1258 return res.p if res else 0
1259
1260 # Releases
1261
1262 def get_releases_map(self, when=None):
1263 if when:
1264 raise NotImplementedError
1265
1266 else:
1267 res = self.db.query("""
1268 SELECT
1269 blob->'system'->'release' AS release,
1270 fireinfo_percentage(
1271 COUNT(*), SUM(COUNT(*)) OVER ()
1272 ) AS p
1273 FROM
1274 fireinfo
1275 WHERE
1276 expired_at IS NULL
1277 AND
1278 blob IS NOT NULL
1279 AND
1280 blob->'system'->'release' IS NOT NULL
1281 GROUP BY
1282 blob->'system'->'release'
1283 """)
1284
1285 return { row.release : row.p for row in res }
1286
1287 # Kernels
1288
1289 def get_kernels_map(self, when=None):
1290 if when:
1291 raise NotImplementedError
1292
1293 else:
1294 res = self.db.query("""
1295 SELECT
1296 blob->'system'->'kernel' AS kernel,
1297 fireinfo_percentage(
1298 COUNT(*), SUM(COUNT(*)) OVER ()
1299 ) AS p
1300 FROM
1301 fireinfo
1302 WHERE
1303 expired_at IS NULL
1304 AND
1305 blob IS NOT NULL
1306 AND
1307 blob->'system'->'kernel' IS NOT NULL
1308 GROUP BY
1309 blob->'system'->'kernel'
1310 """)
1311
1312 return { row.kernel : row.p for row in res }
1313
1314 subsystem2class = {
1315 "pci" : hwdata.PCI(),
1316 "usb" : hwdata.USB(),
1317 }
1318
1319 def get_vendor_string(self, subsystem, vendor_id):
1320 try:
1321 cls = self.subsystem2class[subsystem]
1322 except KeyError:
1323 return ""
1324
1325 return cls.get_vendor(vendor_id) or ""
1326
1327 def get_model_string(self, subsystem, vendor_id, model_id):
1328 try:
1329 cls = self.subsystem2class[subsystem]
1330 except KeyError:
1331 return ""
1332
1333 return cls.get_device(vendor_id, model_id) or ""
1334
1335 def get_vendor_list(self, when=None):
1336 if when:
1337 raise NotImplementedError
1338
1339 else:
1340 res = self.db.query("""
1341 WITH devices AS (
1342 SELECT
1343 jsonb_array_elements(blob->'devices') AS device
1344 FROM
1345 fireinfo
1346 WHERE
1347 expired_at IS NULL
1348 AND
1349 blob IS NOT NULL
1350 AND
1351 blob->'devices' IS NOT NULL
1352 AND
1353 jsonb_typeof(blob->'devices') = 'array'
1354 )
1355
1356 SELECT
1357 devices.device->'subsystem' AS subsystem,
1358 devices.device->'vendor' AS vendor
1359 FROM
1360 devices
1361 WHERE
1362 devices.device->'subsystem' IS NOT NULL
1363 AND
1364 devices.device->'vendor' IS NOT NULL
1365 AND
1366 NOT devices.device->>'driver' = 'usb'
1367 GROUP BY
1368 subsystem, vendor
1369 """)
1370
1371 vendors = {}
1372
1373 for row in res:
1374 vendor = self.get_vendor_string(row.subsystem, row.vendor) or row.vendor
1375
1376 # Drop if vendor could not be determined
1377 if vendor is None:
1378 continue
1379
1380 try:
1381 vendors[vendor].append((row.subsystem, row.vendor))
1382 except KeyError:
1383 vendors[vendor] = [(row.subsystem, row.vendor)]
1384
1385 return vendors
1386
1387 def _get_devices(self, query, *args, **kwargs):
1388 res = self.db.query(query, *args, **kwargs)
1389
1390 return [Device(self.backend, blob) for blob in res]
1391
1392 def get_devices_by_vendor(self, subsystem, vendor, when=None):
1393 if when:
1394 raise NotImplementedError
1395
1396 else:
1397 return self._get_devices("""
1398 WITH devices AS (
1399 SELECT
1400 jsonb_array_elements(blob->'devices') AS device
1401 FROM
1402 fireinfo
1403 WHERE
1404 expired_at IS NULL
1405 AND
1406 blob IS NOT NULL
1407 AND
1408 blob->'devices' IS NOT NULL
1409 AND
1410 jsonb_typeof(blob->'devices') = 'array'
1411 )
1412
1413 SELECT
1414 device.deviceclass,
1415 device.subsystem,
1416 device.vendor,
1417 device.model,
1418 device.driver
1419 FROM
1420 devices,
1421 jsonb_to_record(devices.device) AS device(
1422 deviceclass text,
1423 subsystem text,
1424 vendor text,
1425 sub_vendor text,
1426 model text,
1427 sub_model text,
1428 driver text
1429 )
1430 WHERE
1431 devices.device->>'subsystem' = %s
1432 AND
1433 devices.device->>'vendor' = %s
1434 AND
1435 NOT devices.device->>'driver' = 'usb'
1436 GROUP BY
1437 device.deviceclass,
1438 device.subsystem,
1439 device.vendor,
1440 device.model,
1441 device.driver
1442 """, subsystem, vendor,
1443 )
1444
1445 def get_devices_by_driver(self, driver, when=None):
1446 if when:
1447 raise NotImplementedError
1448
1449 else:
1450 return self._get_devices("""
1451 WITH devices AS (
1452 SELECT
1453 jsonb_array_elements(blob->'devices') AS device
1454 FROM
1455 fireinfo
1456 WHERE
1457 expired_at IS NULL
1458 AND
1459 blob IS NOT NULL
1460 AND
1461 blob->'devices' IS NOT NULL
1462 AND
1463 jsonb_typeof(blob->'devices') = 'array'
1464 )
1465
1466 SELECT
1467 device.deviceclass,
1468 device.subsystem,
1469 device.vendor,
1470 device.model,
1471 device.driver
1472 FROM
1473 devices,
1474 jsonb_to_record(devices.device) AS device(
1475 deviceclass text,
1476 subsystem text,
1477 vendor text,
1478 sub_vendor text,
1479 model text,
1480 sub_model text,
1481 driver text
1482 )
1483 WHERE
1484 devices.device->>'driver' = '%s'
1485 GROUP BY
1486 device.deviceclass,
1487 device.subsystem,
1488 device.vendor,
1489 device.model,
1490 device.driver
1491 """, driver,
1492 )