]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/fireinfo.py
ff672043e1db103558e3bd8b30f3533d98cd1d13
12 from .misc
import Object
13 from .decorators
import *
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",
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"),
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"),
50 (r
"Geode\(TM\) Integrated Processor by AMD PCS", r
"AMD Geode"),
51 (r
"(Geode).*", r
"\1"),
53 (r
"Mobile AMD (Athlon|Sempron)\(tm\) Processor (\d+\+?)", r
"AMD \1-M \2"),
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"),
73 (r
"Geode\(TM\) Integrated Processor by National Semi", r
"NSC Geode"),
76 (r
"(VIA \w*).*", r
"\1"),
79 (r
"QEMU Virtual CPU version .*", r
"QEMU CPU"),
82 (r
"Feroceon .*", r
"ARM Feroceon"),
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",
100 "pattern" : r
"^[a-z0-9\_]{,8}$",
106 "type" : ["integer", "null"],
112 "pattern" : r
"^.{,24}$",
116 "type" : ["integer", "null"],
119 "type" : ["string", "null"],
120 "pattern" : r
"^.{,80}$",
126 "type" : ["integer", "null"],
130 "pattern" : r
"^.{,80}$",
133 "additionalProperties" : False,
154 "type" : ["string", "null"],
155 "pattern" : r
"^.{,20}$",
158 "type" : ["string", "null"],
159 "pattern" : r
"^.{,24}$",
163 "pattern" : r
"^[a-z0-9]{4}$",
166 "type" : ["string", "null"],
167 "pattern" : r
"^[a-z0-9]{4}$",
170 "type" : ["string", "null"],
171 "pattern" : r
"^[a-z0-9]{4}$",
175 "pattern" : r
"^[a-z]{3}$",
178 "type" : ["string", "null"],
179 "pattern" : r
"^[a-z0-9]{4}$",
182 "additionalProperties" : False,
210 "additionalProperties" : False,
219 "pattern" : r
"^.{,40}$",
223 "pattern" : r
"^[a-z]{2}(\.utf8)?$",
229 "type" : ["string", "null"],
230 "pattern" : r
"^.{,80}$",
234 "pattern" : r
"^.{,80}$",
237 "type" : ["number", "null"],
240 "type" : ["string", "null"],
241 "pattern" : r
"^.{,80}$",
247 "additionalProperties" : False,
266 "pattern" : r
"^.{,40}$",
269 "additionalProperties" : False,
280 "additionalProperties" : False,
289 class Network(Object
):
290 def init(self
, blob
):
296 for zone
in ("red", "green", "orange", "blue"):
297 if self
.has_zone(zone
):
302 def has_zone(self
, name
):
303 return self
.blob
.get(name
, False)
307 return self
.has_zone("red")
311 return self
.has_zone("green")
314 def has_orange(self
):
315 return self
.has_zone("orange")
319 return self
.has_zone("blue")
322 class Processor(Object
):
323 def init(self
, blob
):
329 if self
.model_string
and not self
.model_string
.startswith(self
.vendor
):
330 s
.append(self
.vendor
)
333 s
.append(self
.model_string
or "Generic")
335 if self
.core_count
> 1:
336 s
.append("x%s" % self
.core_count
)
342 return self
.blob
.get("arch")
346 vendor
= self
.blob
.get("vendor")
349 return CPU_VENDORS
[vendor
]
355 return self
.blob
.get("family")
359 return self
.blob
.get("model")
363 return self
.blob
.get("stepping")
366 def model_string(self
):
367 return self
.blob
.get("model_string")
371 return self
.blob
.get("flags")
373 def has_flag(self
, flag
):
374 return flag
in self
.flags
377 if self
.vendor
== "Intel" and self
.family
== 6 and self
.model
in (15, 55, 76, 77):
380 return self
.has_flag("ht")
383 def core_count(self
):
384 return self
.blob
.get("count", 1)
389 return self
.core_count
// 2
391 return self
.core_count
394 def clock_speed(self
):
395 return self
.blob
.get("speed", 0)
397 def format_clock_speed(self
):
398 if not self
.clock_speed
:
401 if self
.clock_speed
< 1000:
402 return "%dMHz" % self
.clock_speed
404 return "%.2fGHz" % round(self
.clock_speed
/ 1000, 2)
408 return self
.__bogomips
411 def capabilities(self
):
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")),
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))
425 caps
.append(("virt", self
.has_flag("vmx") or self
.has_flag("svm")))
429 def format_model(self
):
430 s
= self
.model_string
or ""
432 # Remove everything after the @: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
433 s
, sep
, rest
= s
.partition("@")
435 for pattern
, repl
in CPU_STRINGS
:
436 if re
.match(pattern
, s
) is None:
439 s
= re
.sub(pattern
, repl
, s
)
442 # Otherwise remove the symbols
443 for i
in ("C", "R", "TM", "tm"):
444 s
= s
.replace("(%s)" % i
, "")
446 # Replace too long strings with shorter ones
448 ("Quad-Core Processor", ""),
449 ("Dual-Core Processor", ""),
450 ("Processor", "CPU"),
451 ("processor", "CPU"),
456 # Remove too many spaces
457 s
= " ".join((e
for e
in s
.split() if e
))
462 def friendly_string(self
):
465 model
= self
.format_model()
469 clock_speed
= self
.format_clock_speed()
471 s
.append("@ %s" % clock_speed
)
474 s
.append("x%s" % self
.count
)
479 class Device(Object
):
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"),
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"),
504 "00" : N_("Unclassified"),
505 "01" : N_("Multimedia"),
506 "02" : N_("Communication"),
507 "03" : N_("Input device"),
508 "05" : N_("Generic system peripheral"),
510 "07" : N_("Printer"),
511 "08" : N_("Mass storage"),
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"),
526 def init(self
, blob
):
530 return "<%s vendor=%s model=%s>" % (self
.__class
__.__name
__,
531 self
.vendor_string
, self
.model_string
)
533 def __eq__(self
, other
):
534 if isinstance(other
, self
.__class
__):
535 return self
.blob
== other
.blob
537 return NotImplemented
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
547 return NotImplemented
549 def is_showable(self
):
550 if self
.driver
in ("usb", "pcieport", "hub"):
557 return self
.blob
.get("subsystem")
561 return self
.blob
.get("model")
564 def model_string(self
):
565 return self
.fireinfo
.get_model_string(self
.subsystem
, self
.vendor
, self
.model
)
569 return self
.blob
.get("vendor")
572 def vendor_string(self
):
573 return self
.fireinfo
.get_vendor_string(self
.subsystem
, self
.vendor
)
577 return self
.blob
.get("driver")
581 classid
= self
.blob
.get("deviceclass")
583 if self
.subsystem
== "pci":
584 classid
= classid
[:-4]
585 if len(classid
) == 1:
586 classid
= "0%s" % classid
588 elif self
.subsystem
== "usb" and classid
:
589 classid
= classid
.split("/")[0]
590 classid
= "%02x" % int(classid
)
593 return self
.classid2name
[self
.subsystem
][classid
]
598 class System(Object
):
599 def init(self
, blob
):
604 return self
.blob
.get("language")
608 return self
.blob
.get("vendor")
612 return self
.blob
.get("model")
616 return self
.blob
.get("release")
620 return self
.blob
.get("storage_size", 0)
622 def is_virtual(self
):
623 return self
.blob
.get("virtual", False)
626 class Hypervisor(Object
):
627 def init(self
, blob
):
635 return self
.blob
.get("vendor")
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
647 self
.last_updated_at
= last_updated_at
648 self
.country_code
= country_code
651 return "<%s %s>" % (self
.__class
__.__name
__, self
.profile_id
)
653 def is_showable(self
):
654 return True if self
.blob
else False
659 An alias for the profile ID
661 return self
.profile_id
667 return self
.country_code
670 def location_string(self
):
671 return self
.backend
.get_country_name(self
.location
) or self
.location
677 return [Device(self
.backend
, blob
) for blob
in self
.blob
.get("devices", [])]
683 return System(self
.backend
, self
.blob
.get("system", {}))
689 return Processor(self
.backend
, self
.blob
.get("cpu", {}))
695 return self
.blob
.get("memory")
698 def friendly_memory(self
):
699 return util
.format_size(self
.memory
or 0)
703 def is_virtual(self
):
704 return self
.system
.is_virtual()
707 def hypervisor(self
):
708 return Hypervisor(self
.backend
, self
.blob
.get("hypervisor"))
714 return Network(self
.backend
, self
.blob
.get("network", {}))
717 class Fireinfo(Object
):
718 async def expire(self
):
720 Called to expire any profiles that have not been updated in a fortnight
722 self
.db
.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
723 WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime
.timedelta(days
=14))
725 def _get_profile(self
, query
, *args
, **kwargs
):
726 res
= self
.db
.get(query
, *args
, **kwargs
)
729 return Profile(self
.backend
, **res
)
731 def get_profile_count(self
, when
=None):
733 res
= self
.db
.get("""
748 res
= self
.db
.get("""
758 return res
.count
if res
else 0
762 def get_profile(self
, profile_id
, when
=None):
764 return self
._get
_profile
("""
772 %s BETWEEN created_at AND expired_at
776 return self
._get
_profile
("""
790 def handle_profile(self
, profile_id
, blob
, country_code
=None, asn
=None, when
=None):
791 private_id
= blob
.get("private_id", None)
794 now
= datetime
.datetime
.utcnow()
796 # Fetch the profile version
797 version
= blob
.get("profile_version")
799 # Extract the profile
800 profile
= blob
.get("profile")
803 # Validate the profile
804 self
._validate
(profile_id
, version
, profile
)
806 # Pre-process the profile
807 profile
= self
._preprocess
(profile
)
809 # Fetch the previous profile
810 prev
= self
.get_profile(profile_id
)
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
)
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
)
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
)
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
)
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
)
837 # Serialise the profile
839 profile
= json
.dumps(profile
)
841 # Store the new profile
862 """, profile_id
, private_id
, version
, profile
, country_code
, asn
,
865 def _validate(self
, profile_id
, version
, blob
):
870 raise ValueError("Unsupported profile version")
874 return jsonschema
.validate(blob
, schema
=PROFILE_SCHEMA
)
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
880 def _preprocess(self
, blob
):
882 Modifies the profile before storing it
884 # Remove the architecture from the release string
885 blob
["system"]["release"]= self
._filter
_release
(blob
["system"]["release"])
889 def _filter_release(self
, release
):
891 Removes the arch part
893 r
= [e
for e
in release
.split() if e
]
895 for s
in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
906 def get_random_profile(self
, when
=None):
908 return self
._get
_profile
("""
930 return self
._get
_profile
("""
945 def get_active_profiles(self
, when
=None):
947 raise NotImplementedError
950 res
= self
.db
.get("""
952 COUNT(*) AS total_profiles,
953 COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
961 return res
.active_profiles
, res
.total_profiles
963 def get_geo_location_map(self
, when
=None):
965 res
= self
.db
.query("""
969 COUNT(*), SUM(COUNT(*)) OVER ()
982 country_code IS NOT NULL
987 res
= self
.db
.query("""
991 COUNT(*), SUM(COUNT(*)) OVER ()
998 country_code IS NOT NULL
1003 return { row
.country_code
: row
.p
for row
in res
}
1005 def get_asn_map(self
, when
=None):
1007 res
= self
.db
.query("""
1010 fireinfo_percentage(
1011 COUNT(*), SUM(COUNT(*)) OVER ()
1030 res
= self
.db
.query("""
1033 fireinfo_percentage(
1034 COUNT(*), SUM(COUNT(*)) OVER ()
1047 return { self
.backend
.location
.get_as(row
.asn
) : (row
.c
, row
.p
) for row
in res
}
1050 def cpu_vendors(self
):
1051 res
= self
.db
.query("""
1053 blob->'cpu'->'vendor' AS vendor
1057 blob->'cpu'->'vendor' IS NOT NULL
1061 return sorted((CPU_VENDORS
.get(row
.vendor
, row
.vendor
) for row
in res
))
1063 def get_cpu_vendors_map(self
, when
=None):
1065 raise NotImplementedError
1068 res
= self
.db
.query("""
1070 blob->'cpu'->'vendor' AS vendor,
1071 fireinfo_percentage(
1072 COUNT(*), SUM(COUNT(*)) OVER ()
1081 blob->'cpu'->'vendor' IS NOT NULL
1083 blob->'cpu'->'vendor'
1086 return { CPU_VENDORS
.get(row
.vendor
, row
.vendor
) : row
.p
for row
in res
}
1088 def get_cpu_flags_map(self
, when
=None):
1090 raise NotImplementedError
1093 res
= self
.db
.query("""
1094 WITH arch_flags AS (
1096 ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
1097 blob->'cpu'->'arch' AS arch,
1098 blob->'cpu'->'flags' AS flags
1104 blob->'cpu'->'arch' IS NOT NULL
1106 blob->'cpu'->'flags' IS NOT NULL
1108 -- Filter out virtual systems
1110 CAST((blob->'system'->'virtual') AS boolean) IS FALSE
1116 fireinfo_percentage(
1122 arch_flags __arch_flags
1124 arch_flags.arch = __arch_flags.arch
1128 arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
1137 result
[row
.arch
][row
.flag
] = row
.p
1139 result
[row
.arch
] = { row
.flag
: row
.p
}
1143 def get_average_memory_amount(self
, when
=None):
1145 res
= self
.db
.get("""
1148 CAST(blob->'system'->'memory' AS numeric)
1162 res
= self
.db
.get("""
1165 CAST(blob->'system'->'memory' AS numeric)
1173 return res
.memory
if res
else 0
1175 def get_arch_map(self
, when
=None):
1177 raise NotImplementedError
1180 res
= self
.db
.query("""
1182 blob->'cpu'->'arch' AS arch,
1183 fireinfo_percentage(
1184 COUNT(*), SUM(COUNT(*)) OVER ()
1191 blob->'cpu'->'arch' IS NOT NULL
1196 return { row
.arch
: row
.p
for row
in res
}
1200 def get_hypervisor_map(self
, when
=None):
1202 raise NotImplementedError
1204 res
= self
.db
.query("""
1206 blob->'hypervisor'->'vendor' AS vendor,
1207 fireinfo_percentage(
1208 COUNT(*), SUM(COUNT(*)) OVER ()
1215 CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1217 blob->'hypervisor'->'vendor' IS NOT NULL
1219 blob->'hypervisor'->'vendor'
1222 return { row
.vendor
: row
.p
for row
in res
}
1224 def get_virtual_ratio(self
, when
=None):
1226 raise NotImplementedError
1229 res
= self
.db
.get("""
1231 fireinfo_percentage(
1233 WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1245 return res
.p
if res
else 0
1249 def get_releases_map(self
, when
=None):
1251 raise NotImplementedError
1254 res
= self
.db
.query("""
1256 blob->'system'->'release' AS release,
1257 fireinfo_percentage(
1258 COUNT(*), SUM(COUNT(*)) OVER ()
1267 blob->'system'->'release' IS NOT NULL
1269 blob->'system'->'release'
1272 return { row
.release
: row
.p
for row
in res
}
1276 def get_kernels_map(self
, when
=None):
1278 raise NotImplementedError
1281 res
= self
.db
.query("""
1284 blob->'system'->'kernel_release',
1285 blob->'system'->'kernel'
1287 fireinfo_percentage(
1288 COUNT(*), SUM(COUNT(*)) OVER ()
1298 blob->'system'->'kernel_release' IS NOT NULL
1300 blob->'system'->'kernel' IS NOT NULL
1304 blob->'system'->'kernel_release',
1305 blob->'system'->'kernel'
1309 return { row
.kernel
: row
.p
for row
in res
}
1312 "pci" : hwdata
.PCI(),
1313 "usb" : hwdata
.USB(),
1316 def get_vendor_string(self
, subsystem
, vendor_id
):
1318 cls
= self
.subsystem2class
[subsystem
]
1322 return cls
.get_vendor(vendor_id
) or ""
1324 def get_model_string(self
, subsystem
, vendor_id
, model_id
):
1326 cls
= self
.subsystem2class
[subsystem
]
1330 return cls
.get_device(vendor_id
, model_id
) or ""
1332 def get_vendor_list(self
, when
=None):
1334 raise NotImplementedError
1337 res
= self
.db
.query("""
1340 jsonb_array_elements(blob->'devices') AS device
1348 blob->'devices' IS NOT NULL
1350 jsonb_typeof(blob->'devices') = 'array'
1354 devices.device->'subsystem' AS subsystem,
1355 devices.device->'vendor' AS vendor
1359 devices.device->'subsystem' IS NOT NULL
1361 devices.device->'vendor' IS NOT NULL
1363 NOT devices.device->>'driver' = 'usb'
1371 vendor
= self
.get_vendor_string(row
.subsystem
, row
.vendor
) or row
.vendor
1373 # Drop if vendor could not be determined
1378 vendors
[vendor
].append((row
.subsystem
, row
.vendor
))
1380 vendors
[vendor
] = [(row
.subsystem
, row
.vendor
)]
1384 def _get_devices(self
, query
, *args
, **kwargs
):
1385 res
= self
.db
.query(query
, *args
, **kwargs
)
1387 return [Device(self
.backend
, blob
) for blob
in res
]
1389 def get_devices_by_vendor(self
, subsystem
, vendor
, when
=None):
1391 raise NotImplementedError
1394 return self
._get
_devices
("""
1397 jsonb_array_elements(blob->'devices') AS device
1405 blob->'devices' IS NOT NULL
1407 jsonb_typeof(blob->'devices') = 'array'
1418 jsonb_to_record(devices.device) AS device(
1428 devices.device->>'subsystem' = %s
1430 devices.device->>'vendor' = %s
1432 NOT devices.device->>'driver' = 'usb'
1439 """, subsystem
, vendor
,
1442 def get_devices_by_driver(self
, driver
, when
=None):
1444 raise NotImplementedError
1447 return self
._get
_devices
("""
1450 jsonb_array_elements(blob->'devices') AS device
1458 blob->'devices' IS NOT NULL
1460 jsonb_typeof(blob->'devices') = 'array'
1471 jsonb_to_record(devices.device) AS device(
1481 devices.device->>'driver' = %s