]> git.ipfire.org Git - ipfire.org.git/blob - src/backend/fireinfo.py
wiki: Only match usernames when a word starts with @
[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", "null"],
117 },
118 "model_string" : {
119 "type" : ["string", "null"],
120 "pattern" : r"^.{,80}$",
121 },
122 "speed" : {
123 "type" : "number",
124 },
125 "stepping" : {
126 "type" : ["integer", "null"],
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", "null"],
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", "null"],
238 },
239 "vendor" : {
240 "type" : ["string", "null"],
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 arch(self):
342 return self.blob.get("arch")
343
344 @property
345 def vendor(self):
346 vendor = self.blob.get("vendor")
347
348 try:
349 return CPU_VENDORS[vendor]
350 except KeyError:
351 return vendor
352
353 @property
354 def family(self):
355 return self.blob.get("family")
356
357 @property
358 def model(self):
359 return self.blob.get("model")
360
361 @property
362 def stepping(self):
363 return self.blob.get("stepping")
364
365 @property
366 def model_string(self):
367 return self.blob.get("model_string")
368
369 @property
370 def flags(self):
371 return self.blob.get("flags")
372
373 def has_flag(self, flag):
374 return flag in self.flags
375
376 def uses_ht(self):
377 if self.vendor == "Intel" and self.family == 6 and self.model in (15, 55, 76, 77):
378 return False
379
380 return self.has_flag("ht")
381
382 @property
383 def core_count(self):
384 return self.blob.get("count", 1)
385
386 @property
387 def count(self):
388 if self.uses_ht():
389 return self.core_count // 2
390
391 return self.core_count
392
393 @property
394 def clock_speed(self):
395 return self.blob.get("speed", 0)
396
397 def format_clock_speed(self):
398 if not self.clock_speed:
399 return
400
401 if self.clock_speed < 1000:
402 return "%dMHz" % self.clock_speed
403
404 return "%.2fGHz" % round(self.clock_speed / 1000, 2)
405
406 @property
407 def bogomips(self):
408 return self.__bogomips
409
410 @property
411 def capabilities(self):
412 caps = [
413 ("64bit", self.has_flag("lm")),
414 ("aes", self.has_flag("aes")),
415 ("nx", self.has_flag("nx")),
416 ("pae", self.has_flag("pae") or self.has_flag("lpae")),
417 ("rdrand", self.has_flag("rdrand")),
418 ]
419
420 # If the system is already running in a virtual environment,
421 # we cannot correctly detect if the CPU supports svm or vmx
422 if self.has_flag("hypervisor"):
423 caps.append(("virt", None))
424 else:
425 caps.append(("virt", self.has_flag("vmx") or self.has_flag("svm")))
426
427 return caps
428
429 def format_model(self):
430 s = self.model_string or ""
431
432 # Remove everything after the @: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
433 s, sep, rest = s.partition("@")
434
435 for pattern, repl in CPU_STRINGS:
436 if re.match(pattern, s) is None:
437 continue
438
439 s = re.sub(pattern, repl, s)
440 break
441
442 # Otherwise remove the symbols
443 for i in ("C", "R", "TM", "tm"):
444 s = s.replace("(%s)" % i, "")
445
446 # Replace too long strings with shorter ones
447 pairs = (
448 ("Quad-Core Processor", ""),
449 ("Dual-Core Processor", ""),
450 ("Processor", "CPU"),
451 ("processor", "CPU"),
452 )
453 for k, v in pairs:
454 s = s.replace(k, v)
455
456 # Remove too many spaces
457 s = " ".join((e for e in s.split() if e))
458
459 return s
460
461 @property
462 def friendly_string(self):
463 s = []
464
465 model = self.format_model()
466 if model:
467 s.append(model)
468
469 clock_speed = self.format_clock_speed()
470 if clock_speed:
471 s.append("@ %s" % clock_speed)
472
473 if self.count > 1:
474 s.append("x%s" % self.count)
475
476 return " ".join(s)
477
478
479 class Device(Object):
480 classid2name = {
481 "pci" : {
482 "00" : N_("Unclassified"),
483 "01" : N_("Mass storage"),
484 "02" : N_("Network"),
485 "03" : N_("Display"),
486 "04" : N_("Multimedia"),
487 "05" : N_("Memory controller"),
488 "06" : N_("Bridge"),
489 "07" : N_("Communication"),
490 "08" : N_("Generic system peripheral"),
491 "09" : N_("Input device"),
492 "0a" : N_("Docking station"),
493 "0b" : N_("Processor"),
494 "0c" : N_("Serial bus"),
495 "0d" : N_("Wireless"),
496 "0e" : N_("Intelligent controller"),
497 "0f" : N_("Satellite communications controller"),
498 "10" : N_("Encryption"),
499 "11" : N_("Signal processing controller"),
500 "ff" : N_("Unassigned class"),
501 },
502
503 "usb" : {
504 "00" : N_("Unclassified"),
505 "01" : N_("Multimedia"),
506 "02" : N_("Communication"),
507 "03" : N_("Input device"),
508 "05" : N_("Generic system peripheral"),
509 "06" : N_("Image"),
510 "07" : N_("Printer"),
511 "08" : N_("Mass storage"),
512 "09" : N_("Hub"),
513 "0a" : N_("Communication"),
514 "0b" : N_("Smart card"),
515 "0d" : N_("Encryption"),
516 "0e" : N_("Display"),
517 "0f" : N_("Personal Healthcare"),
518 "dc" : N_("Diagnostic Device"),
519 "e0" : N_("Wireless"),
520 "ef" : N_("Unclassified"),
521 "fe" : N_("Unclassified"),
522 "ff" : N_("Unclassified"),
523 }
524 }
525
526 def init(self, blob):
527 self.blob = blob
528
529 def __repr__(self):
530 return "<%s vendor=%s model=%s>" % (self.__class__.__name__,
531 self.vendor_string, self.model_string)
532
533 def __eq__(self, other):
534 if isinstance(other, self.__class__):
535 return self.blob == other.blob
536
537 return NotImplemented
538
539 def __lt__(self, other):
540 if isinstance(other, self.__class__):
541 return self.cls < other.cls or \
542 self.vendor_string < other.vendor_string or \
543 self.vendor < other.vendor or \
544 self.model_string < other.model_string or \
545 self.model < other.model
546
547 return NotImplemented
548
549 def is_showable(self):
550 if self.driver in ("usb", "pcieport", "hub"):
551 return False
552
553 return True
554
555 @property
556 def subsystem(self):
557 return self.blob.get("subsystem")
558
559 @property
560 def model(self):
561 return self.blob.get("model")
562
563 @lazy_property
564 def model_string(self):
565 return self.fireinfo.get_model_string(self.subsystem, self.vendor, self.model)
566
567 @property
568 def vendor(self):
569 return self.blob.get("vendor")
570
571 @lazy_property
572 def vendor_string(self):
573 return self.fireinfo.get_vendor_string(self.subsystem, self.vendor)
574
575 @property
576 def driver(self):
577 return self.blob.get("driver")
578
579 @lazy_property
580 def cls(self):
581 classid = self.blob.get("deviceclass")
582
583 if self.subsystem == "pci":
584 classid = classid[:-4]
585 if len(classid) == 1:
586 classid = "0%s" % classid
587
588 elif self.subsystem == "usb" and classid:
589 classid = classid.split("/")[0]
590 classid = "%02x" % int(classid)
591
592 try:
593 return self.classid2name[self.subsystem][classid]
594 except KeyError:
595 return "N/A"
596
597
598 class System(Object):
599 def init(self, blob):
600 self.blob = blob
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 # Memory
619
620 @property
621 def memory(self):
622 return self.blob.get("memory") * 1024
623
624 @property
625 def friendly_memory(self):
626 return util.format_size(self.memory or 0)
627
628 @property
629 def storage(self):
630 return self.blob.get("storage_size", 0)
631
632 def is_virtual(self):
633 return self.blob.get("virtual", False)
634
635
636 class Hypervisor(Object):
637 def init(self, blob):
638 self.blob = blob
639
640 def __str__(self):
641 return self.vendor
642
643 @property
644 def vendor(self):
645 return self.blob.get("vendor")
646
647
648 class Profile(Object):
649 def init(self, profile_id, private_id, created_at, expired_at, version, blob,
650 last_updated_at, country_code, **kwargs):
651 self.profile_id = profile_id
652 self.private_id = private_id
653 self.created_at = created_at
654 self.expired_at = expired_at
655 self.version = version
656 self.blob = blob
657 self.last_updated_at = last_updated_at
658 self.country_code = country_code
659
660 def __repr__(self):
661 return "<%s %s>" % (self.__class__.__name__, self.profile_id)
662
663 def is_showable(self):
664 return True if self.blob else False
665
666 @property
667 def public_id(self):
668 """
669 An alias for the profile ID
670 """
671 return self.profile_id
672
673 # Location
674
675 @property
676 def location(self):
677 return self.country_code
678
679 @property
680 def location_string(self):
681 return self.backend.get_country_name(self.location) or self.location
682
683 # Devices
684
685 @lazy_property
686 def devices(self):
687 return [Device(self.backend, blob) for blob in self.blob.get("devices", [])]
688
689 # System
690
691 @lazy_property
692 def system(self):
693 return System(self.backend, self.blob.get("system", {}))
694
695 # Processor
696
697 @property
698 def processor(self):
699 return Processor(self.backend, self.blob.get("cpu", {}))
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 # Profiles
761
762 def get_profile(self, profile_id, when=None):
763 if when:
764 return self._get_profile("""
765 SELECT
766 *
767 FROM
768 fireinfo
769 WHERE
770 profile_id = %s
771 AND
772 %s BETWEEN created_at AND expired_at
773 """, profile_id,
774 )
775
776 return self._get_profile("""
777 SELECT
778 *
779 FROM
780 fireinfo
781 WHERE
782 profile_id = %s
783 AND
784 expired_at IS NULL
785 """, profile_id,
786 )
787
788 # Handle profile
789
790 def handle_profile(self, profile_id, blob, country_code=None, asn=None, when=None):
791 private_id = blob.get("private_id", None)
792 assert private_id
793
794 now = datetime.datetime.utcnow()
795
796 # Fetch the profile version
797 version = blob.get("profile_version")
798
799 # Extract the profile
800 profile = blob.get("profile")
801
802 if profile:
803 # Validate the profile
804 self._validate(profile_id, version, profile)
805
806 # Pre-process the profile
807 profile = self._preprocess(profile)
808
809 # Fetch the previous profile
810 prev = self.get_profile(profile_id)
811
812 if prev:
813 # Check if the private ID matches
814 if not prev.private_id == private_id:
815 logging.error("Private ID for profile %s does not match" % profile_id)
816 return False
817
818 # Check when the last update was
819 elif now - prev.last_updated_at < datetime.timedelta(hours=6):
820 logging.warning("Profile %s has been updated too soon" % profile_id)
821 return False
822
823 # Check if the profile has changed
824 elif prev.version == version and prev.blob == blob:
825 logging.debug("Profile %s has not changed" % profile_id)
826
827 # Update the timestamp
828 self.db.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
829 WHERE profile_id = %s AND expired_at IS NULL", profile_id)
830
831 return True
832
833 # Delete the previous profile
834 self.db.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
835 WHERE profile_id = %s AND expired_at IS NULL", profile_id)
836
837 # Serialise the profile
838 if profile:
839 profile = json.dumps(profile)
840
841 # Store the new profile
842 self.db.execute("""
843 INSERT INTO
844 fireinfo
845 (
846 profile_id,
847 private_id,
848 version,
849 blob,
850 country_code,
851 asn
852 )
853 VALUES
854 (
855 %s,
856 %s,
857 %s,
858 %s,
859 %s,
860 %s
861 )
862 """, profile_id, private_id, version, profile, country_code, asn,
863 )
864
865 def _validate(self, profile_id, version, blob):
866 """
867 Validate the profile
868 """
869 if not version == 0:
870 raise ValueError("Unsupported profile version")
871
872 # Validate the blob
873 try:
874 return jsonschema.validate(blob, schema=PROFILE_SCHEMA)
875
876 # Raise a ValueError instead which is easier to handle later on
877 except jsonschema.exceptions.ValidationError as e:
878 raise ValueError("%s" % e) from e
879
880 def _preprocess(self, blob):
881 """
882 Modifies the profile before storing it
883 """
884 # Remove the architecture from the release string
885 blob["system"]["release"]= self._filter_release(blob["system"]["release"])
886
887 return blob
888
889 def _filter_release(self, release):
890 """
891 Removes the arch part
892 """
893 r = [e for e in release.split() if e]
894
895 for s in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
896 try:
897 r.remove(s)
898 break
899 except ValueError:
900 pass
901
902 return " ".join(r)
903
904 # Data outputs
905
906 def get_random_profile(self, when=None):
907 if when:
908 return self._get_profile("""
909 SELECT
910 *
911 FROM
912 fireinfo
913 WHERE
914 created_at <= %s
915 AND
916 (
917 expired_at IS NULL
918 OR
919 expired_at > %s
920 )
921 AND
922 blob IS NOT NULL
923 ORDER BY
924 RANDOM()
925 LIMIT
926 1
927 """, when, when,
928 )
929
930 return self._get_profile("""
931 SELECT
932 *
933 FROM
934 fireinfo
935 WHERE
936 expired_at IS NULL
937 AND
938 blob IS NOT NULL
939 ORDER BY
940 RANDOM()
941 LIMIT
942 1
943 """)
944
945 def get_active_profiles(self, when=None):
946 if when:
947 raise NotImplementedError
948
949 else:
950 res = self.db.get("""
951 SELECT
952 COUNT(*) AS total_profiles,
953 COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
954 FROM
955 fireinfo
956 WHERE
957 expired_at IS NULL
958 """)
959
960 if res:
961 return res.active_profiles, res.total_profiles
962
963 def get_geo_location_map(self, when=None):
964 if when:
965 res = self.db.query("""
966 SELECT
967 country_code,
968 fireinfo_percentage(
969 COUNT(*), SUM(COUNT(*)) OVER ()
970 ) AS p
971 FROM
972 fireinfo
973 WHERE
974 created_at <= %s
975 AND
976 (
977 expired_at IS NULL
978 OR
979 expired_at > %s
980 )
981 AND
982 country_code IS NOT NULL
983 GROUP BY
984 country_code
985 """, when, when)
986 else:
987 res = self.db.query("""
988 SELECT
989 country_code,
990 fireinfo_percentage(
991 COUNT(*), SUM(COUNT(*)) OVER ()
992 ) AS p
993 FROM
994 fireinfo
995 WHERE
996 expired_at IS NULL
997 AND
998 country_code IS NOT NULL
999 GROUP BY
1000 country_code
1001 """)
1002
1003 return { row.country_code : row.p for row in res }
1004
1005 def get_asn_map(self, when=None):
1006 if when:
1007 res = self.db.query("""
1008 SELECT
1009 asn,
1010 fireinfo_percentage(
1011 COUNT(*), SUM(COUNT(*)) OVER ()
1012 ) AS p,
1013 COUNT(*) AS c
1014 FROM
1015 fireinfo
1016 WHERE
1017 created_at <= %s
1018 AND
1019 (
1020 expired_at IS NULL
1021 OR
1022 expired_at > %s
1023 )
1024 AND
1025 asn IS NOT NULL
1026 GROUP BY
1027 asn
1028 """, when, when)
1029 else:
1030 res = self.db.query("""
1031 SELECT
1032 asn,
1033 fireinfo_percentage(
1034 COUNT(*), SUM(COUNT(*)) OVER ()
1035 ) AS p,
1036 COUNT(*) AS c
1037 FROM
1038 fireinfo
1039 WHERE
1040 expired_at IS NULL
1041 AND
1042 asn IS NOT NULL
1043 GROUP BY
1044 asn
1045 """)
1046
1047 return { self.backend.location.get_as(row.asn) : (row.c, row.p) for row in res }
1048
1049 @property
1050 def cpu_vendors(self):
1051 res = self.db.query("""
1052 SELECT DISTINCT
1053 blob->'cpu'->'vendor' AS vendor
1054 FROM
1055 fireinfo
1056 WHERE
1057 blob->'cpu'->'vendor' IS NOT NULL
1058 """,
1059 )
1060
1061 return sorted((CPU_VENDORS.get(row.vendor, row.vendor) for row in res))
1062
1063 def get_cpu_vendors_map(self, when=None):
1064 if when:
1065 raise NotImplementedError
1066
1067 else:
1068 res = self.db.query("""
1069 SELECT
1070 NULLIF(blob->'cpu'->'vendor', '""'::jsonb) AS vendor,
1071 fireinfo_percentage(
1072 COUNT(*), SUM(COUNT(*)) OVER ()
1073 ) AS p
1074 FROM
1075 fireinfo
1076 WHERE
1077 expired_at IS NULL
1078 AND
1079 blob IS NOT NULL
1080 GROUP BY
1081 NULLIF(blob->'cpu'->'vendor', '""'::jsonb)
1082 """)
1083
1084 return { CPU_VENDORS.get(row.vendor, row.vendor) : row.p for row in res }
1085
1086 def get_cpu_flags_map(self, when=None):
1087 if when:
1088 raise NotImplementedError
1089
1090 else:
1091 res = self.db.query("""
1092 WITH arch_flags AS (
1093 SELECT
1094 ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
1095 blob->'cpu'->'arch' AS arch,
1096 blob->'cpu'->'flags' AS flags
1097 FROM
1098 fireinfo
1099 WHERE
1100 expired_at IS NULL
1101 AND
1102 blob->'cpu'->'arch' IS NOT NULL
1103 AND
1104 blob->'cpu'->'flags' IS NOT NULL
1105
1106 -- Filter out virtual systems
1107 AND
1108 CAST((blob->'system'->'virtual') AS boolean) IS FALSE
1109 )
1110
1111 SELECT
1112 arch,
1113 flag,
1114 fireinfo_percentage(
1115 COUNT(*),
1116 (
1117 SELECT
1118 MAX(id)
1119 FROM
1120 arch_flags __arch_flags
1121 WHERE
1122 arch_flags.arch = __arch_flags.arch
1123 )
1124 ) AS p
1125 FROM
1126 arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
1127 GROUP BY
1128 arch, flag
1129 """)
1130
1131 result = {}
1132
1133 for row in res:
1134 try:
1135 result[row.arch][row.flag] = row.p
1136 except KeyError:
1137 result[row.arch] = { row.flag : row.p }
1138
1139 return result
1140
1141 def get_average_memory_amount(self, when=None):
1142 if when:
1143 res = self.db.get("""
1144 SELECT
1145 AVG(
1146 CAST(blob->'system'->'memory' AS numeric)
1147 ) AS memory
1148 FROM
1149 fireinfo
1150 WHERE
1151 created_at <= %s
1152 AND
1153 (
1154 expired_at IS NULL
1155 OR
1156 expired_at > %s
1157 )
1158 """, when)
1159 else:
1160 res = self.db.get("""
1161 SELECT
1162 AVG(
1163 CAST(blob->'system'->'memory' AS numeric)
1164 ) AS memory
1165 FROM
1166 fireinfo
1167 WHERE
1168 expired_at IS NULL
1169 """,)
1170
1171 return res.memory if res else 0
1172
1173 def get_arch_map(self, when=None):
1174 if when:
1175 raise NotImplementedError
1176
1177 else:
1178 res = self.db.query("""
1179 SELECT
1180 blob->'cpu'->'arch' AS arch,
1181 fireinfo_percentage(
1182 COUNT(*), SUM(COUNT(*)) OVER ()
1183 ) AS p
1184 FROM
1185 fireinfo
1186 WHERE
1187 expired_at IS NULL
1188 AND
1189 blob->'cpu'->'arch' IS NOT NULL
1190 GROUP BY
1191 blob->'cpu'->'arch'
1192 """)
1193
1194 return { row.arch : row.p for row in res }
1195
1196 # Virtual
1197
1198 def get_hypervisor_map(self, when=None):
1199 if when:
1200 raise NotImplementedError
1201 else:
1202 res = self.db.query("""
1203 SELECT
1204 blob->'hypervisor'->'vendor' AS vendor,
1205 fireinfo_percentage(
1206 COUNT(*), SUM(COUNT(*)) OVER ()
1207 ) AS p
1208 FROM
1209 fireinfo
1210 WHERE
1211 expired_at IS NULL
1212 AND
1213 CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1214 AND
1215 blob->'hypervisor'->'vendor' IS NOT NULL
1216 GROUP BY
1217 blob->'hypervisor'->'vendor'
1218 """)
1219
1220 return { row.vendor : row.p for row in res }
1221
1222 def get_virtual_ratio(self, when=None):
1223 if when:
1224 raise NotImplementedError
1225
1226 else:
1227 res = self.db.get("""
1228 SELECT
1229 fireinfo_percentage(
1230 COUNT(*) FILTER (
1231 WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1232 ),
1233 COUNT(*)
1234 ) AS p
1235 FROM
1236 fireinfo
1237 WHERE
1238 expired_at IS NULL
1239 AND
1240 blob IS NOT NULL
1241 """)
1242
1243 return res.p if res else 0
1244
1245 # Releases
1246
1247 def get_releases_map(self, when=None):
1248 if when:
1249 raise NotImplementedError
1250
1251 else:
1252 res = self.db.query("""
1253 SELECT
1254 blob->'system'->'release' AS release,
1255 fireinfo_percentage(
1256 COUNT(*), SUM(COUNT(*)) OVER ()
1257 ) AS p
1258 FROM
1259 fireinfo
1260 WHERE
1261 expired_at IS NULL
1262 AND
1263 blob IS NOT NULL
1264 AND
1265 blob->'system'->'release' IS NOT NULL
1266 GROUP BY
1267 blob->'system'->'release'
1268 """)
1269
1270 return { row.release : row.p for row in res }
1271
1272 # Kernels
1273
1274 def get_kernels_map(self, when=None):
1275 if when:
1276 raise NotImplementedError
1277
1278 else:
1279 res = self.db.query("""
1280 SELECT
1281 COALESCE(
1282 blob->'system'->'kernel_release',
1283 blob->'system'->'kernel'
1284 ) AS kernel,
1285 fireinfo_percentage(
1286 COUNT(*), SUM(COUNT(*)) OVER ()
1287 ) AS p
1288 FROM
1289 fireinfo
1290 WHERE
1291 expired_at IS NULL
1292 AND
1293 blob IS NOT NULL
1294 AND
1295 (
1296 blob->'system'->'kernel_release' IS NOT NULL
1297 OR
1298 blob->'system'->'kernel' IS NOT NULL
1299 )
1300 GROUP BY
1301 COALESCE(
1302 blob->'system'->'kernel_release',
1303 blob->'system'->'kernel'
1304 )
1305 """)
1306
1307 return { row.kernel : row.p for row in res }
1308
1309 subsystem2class = {
1310 "pci" : hwdata.PCI(),
1311 "usb" : hwdata.USB(),
1312 }
1313
1314 def get_vendor_string(self, subsystem, vendor_id):
1315 try:
1316 cls = self.subsystem2class[subsystem]
1317 except KeyError:
1318 return ""
1319
1320 return cls.get_vendor(vendor_id) or ""
1321
1322 def get_model_string(self, subsystem, vendor_id, model_id):
1323 try:
1324 cls = self.subsystem2class[subsystem]
1325 except KeyError:
1326 return ""
1327
1328 return cls.get_device(vendor_id, model_id) or ""
1329
1330 def get_vendor_list(self, when=None):
1331 if when:
1332 raise NotImplementedError
1333
1334 else:
1335 res = self.db.query("""
1336 WITH devices AS (
1337 SELECT
1338 jsonb_array_elements(blob->'devices') AS device
1339 FROM
1340 fireinfo
1341 WHERE
1342 expired_at IS NULL
1343 AND
1344 blob IS NOT NULL
1345 AND
1346 blob->'devices' IS NOT NULL
1347 AND
1348 jsonb_typeof(blob->'devices') = 'array'
1349 )
1350
1351 SELECT
1352 devices.device->'subsystem' AS subsystem,
1353 devices.device->'vendor' AS vendor
1354 FROM
1355 devices
1356 WHERE
1357 devices.device->'subsystem' IS NOT NULL
1358 AND
1359 devices.device->'vendor' IS NOT NULL
1360 AND
1361 NOT devices.device->>'driver' = 'usb'
1362 GROUP BY
1363 subsystem, vendor
1364 """)
1365
1366 vendors = {}
1367
1368 for row in res:
1369 vendor = self.get_vendor_string(row.subsystem, row.vendor) or row.vendor
1370
1371 # Drop if vendor could not be determined
1372 if vendor is None:
1373 continue
1374
1375 try:
1376 vendors[vendor].append((row.subsystem, row.vendor))
1377 except KeyError:
1378 vendors[vendor] = [(row.subsystem, row.vendor)]
1379
1380 return vendors
1381
1382 def _get_devices(self, query, *args, **kwargs):
1383 res = self.db.query(query, *args, **kwargs)
1384
1385 return [Device(self.backend, blob) for blob in res]
1386
1387 def get_devices_by_vendor(self, subsystem, vendor, when=None):
1388 if when:
1389 raise NotImplementedError
1390
1391 else:
1392 return self._get_devices("""
1393 WITH devices AS (
1394 SELECT
1395 jsonb_array_elements(blob->'devices') AS device
1396 FROM
1397 fireinfo
1398 WHERE
1399 expired_at IS NULL
1400 AND
1401 blob IS NOT NULL
1402 AND
1403 blob->'devices' IS NOT NULL
1404 AND
1405 jsonb_typeof(blob->'devices') = 'array'
1406 )
1407
1408 SELECT
1409 device.deviceclass,
1410 device.subsystem,
1411 device.vendor,
1412 device.model,
1413 device.driver
1414 FROM
1415 devices,
1416 jsonb_to_record(devices.device) AS device(
1417 deviceclass text,
1418 subsystem text,
1419 vendor text,
1420 sub_vendor text,
1421 model text,
1422 sub_model text,
1423 driver text
1424 )
1425 WHERE
1426 devices.device->>'subsystem' = %s
1427 AND
1428 devices.device->>'vendor' = %s
1429 AND
1430 NOT devices.device->>'driver' = 'usb'
1431 GROUP BY
1432 device.deviceclass,
1433 device.subsystem,
1434 device.vendor,
1435 device.model,
1436 device.driver
1437 """, subsystem, vendor,
1438 )
1439
1440 def get_devices_by_driver(self, driver, when=None):
1441 if when:
1442 raise NotImplementedError
1443
1444 else:
1445 return self._get_devices("""
1446 WITH devices AS (
1447 SELECT
1448 jsonb_array_elements(blob->'devices') AS device
1449 FROM
1450 fireinfo
1451 WHERE
1452 expired_at IS NULL
1453 AND
1454 blob IS NOT NULL
1455 AND
1456 blob->'devices' IS NOT NULL
1457 AND
1458 jsonb_typeof(blob->'devices') = 'array'
1459 )
1460
1461 SELECT
1462 device.deviceclass,
1463 device.subsystem,
1464 device.vendor,
1465 device.model,
1466 device.driver
1467 FROM
1468 devices,
1469 jsonb_to_record(devices.device) AS device(
1470 deviceclass text,
1471 subsystem text,
1472 vendor text,
1473 sub_vendor text,
1474 model text,
1475 sub_model text,
1476 driver text
1477 )
1478 WHERE
1479 devices.device->>'driver' = %s
1480 GROUP BY
1481 device.deviceclass,
1482 device.subsystem,
1483 device.vendor,
1484 device.model,
1485 device.driver
1486 """, driver,
1487 )