]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/fireinfo.py
635cd08a80b7bd654ada908c6acffe9dfe8e863c
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}$",
112 "pattern" : r
"^.{,24}$",
120 "pattern" : r
"^.{,80}$",
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}$",
179 "pattern" : r
"^[a-z0-9]{4}$",
182 "additionalProperties" : False,
210 "additionalProperties" : False,
219 "pattern" : r
"^.{,40}$",
223 "pattern" : r
"^[a-z]{2}$",
230 "pattern" : r
"^.{,80}$",
234 "pattern" : r
"^.{,80}$",
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 vendor
= self
.blob
.get("vendor")
345 return CPU_VENDORS
[vendor
]
351 return self
.blob
.get("family")
355 return self
.blob
.get("model")
359 return self
.blob
.get("stepping")
362 def model_string(self
):
363 return self
.blob
.get("model_string")
367 return self
.blob
.get("flags")
369 def has_flag(self
, flag
):
370 return flag
in self
.flags
373 if self
.vendor
== "Intel" and self
.family
== 6 and self
.model
in (15, 55, 76, 77):
376 return self
.has_flag("ht")
379 def core_count(self
):
380 return self
.blob
.get("count", 1)
385 return self
.core_count
// 2
387 return self
.core_count
390 def clock_speed(self
):
391 return self
.__clock
_speed
393 def format_clock_speed(self
):
394 if not self
.clock_speed
:
397 if self
.clock_speed
< 1000:
398 return "%dMHz" % self
.clock_speed
400 return "%.2fGHz" % round(self
.clock_speed
/ 1000, 2)
404 return self
.__bogomips
407 def capabilities(self
):
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")),
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))
421 caps
.append(("virt", self
.has_flag("vmx") or self
.has_flag("svm")))
425 def format_model(self
):
426 s
= self
.model_string
or ""
428 # Remove everything after the @: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
429 s
, sep
, rest
= s
.partition("@")
431 for pattern
, repl
in CPU_STRINGS
:
432 if re
.match(pattern
, s
) is None:
435 s
= re
.sub(pattern
, repl
, s
)
438 # Otherwise remove the symbols
439 for i
in ("C", "R", "TM", "tm"):
440 s
= s
.replace("(%s)" % i
, "")
442 # Replace too long strings with shorter ones
444 ("Quad-Core Processor", ""),
445 ("Dual-Core Processor", ""),
446 ("Processor", "CPU"),
447 ("processor", "CPU"),
452 # Remove too many spaces
453 s
= " ".join((e
for e
in s
.split() if e
))
458 def friendly_string(self
):
461 model
= self
.format_model()
465 clock_speed
= self
.format_clock_speed()
467 s
.append("@ %s" % clock_speed
)
470 s
.append("x%s" % self
.count
)
475 class Device(Object
):
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"),
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"),
500 "00" : N_("Unclassified"),
501 "01" : N_("Multimedia"),
502 "02" : N_("Communication"),
503 "03" : N_("Input device"),
504 "05" : N_("Generic system peripheral"),
506 "07" : N_("Printer"),
507 "08" : N_("Mass storage"),
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"),
522 def init(self
, blob
):
526 return "<%s vendor=%s model=%s>" % (self
.__class
__.__name
__,
527 self
.vendor_string
, self
.model_string
)
529 def __eq__(self
, other
):
530 if isinstance(other
, self
.__class
__):
531 return self
.blob
== other
.blob
533 return NotImplemented
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
543 return NotImplemented
545 def is_showable(self
):
546 if self
.driver
in ("usb", "pcieport", "hub"):
553 return self
.blob
.get("subsystem")
557 return self
.blob
.get("model")
560 def model_string(self
):
561 return self
.fireinfo
.get_model_string(self
.subsystem
, self
.vendor
, self
.model
)
565 return self
.blob
.get("vendor")
568 def vendor_string(self
):
569 return self
.fireinfo
.get_vendor_string(self
.subsystem
, self
.vendor
)
573 return self
.blob
.get("driver")
577 classid
= self
.blob
.get("deviceclass")
579 if self
.subsystem
== "pci":
580 classid
= classid
[:-4]
581 if len(classid
) == 1:
582 classid
= "0%s" % classid
584 elif self
.subsystem
== "usb" and classid
:
585 classid
= classid
.split("/")[0]
586 classid
= "%02x" % int(classid
)
589 return self
.classid2name
[self
.subsystem
][classid
]
594 class System(Object
):
595 def init(self
, blob
):
600 return self
.blob
.get("arch")
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
760 def get_profile_histogram(self
):
761 today
= datetime
.date
.today()
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)
766 res
= self
.db
.query("""
771 generate_series(%s, %s, INTERVAL '1 month') date
773 fireinfo ON date >= created_at
774 AND (expired_at IS NULL OR expired_at > date)
779 return { row
.date
: row
.count
for row
in res
}
783 def get_profile(self
, profile_id
, when
=None):
785 return self
._get
_profile
("""
793 %s BETWEEN created_at AND expired_at
797 return self
._get
_profile
("""
811 def handle_profile(self
, profile_id
, blob
, country_code
=None, asn
=None, when
=None):
812 private_id
= blob
.get("private_id", None)
815 now
= datetime
.datetime
.utcnow()
817 # Fetch the profile version
818 version
= blob
.get("profile_version")
820 # Extract the profile
821 profile
= blob
.get("profile")
823 # Validate the profile
824 self
._validate
(profile_id
, version
, profile
)
826 # Pre-process the profile
827 profile
= self
._preprocess
(profile
)
829 # Fetch the previous profile
830 prev
= self
.get_profile(profile_id
)
833 # Check if the private ID matches
834 if not prev
.private_id
== private_id
:
835 logging
.error("Private ID for profile %s does not match" % profile_id
)
838 # Check when the last update was
839 elif now
- prev
.last_updated_at
< datetime
.timedelta(hours
=6):
840 logging
.warning("Profile %s has been updated too soon" % profile_id
)
843 # Check if the profile has changed
844 elif prev
.version
== version
and prev
.blob
== blob
:
845 logging
.debug("Profile %s has not changed" % profile_id
)
847 # Update the timestamp
848 self
.db
.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
849 WHERE profile_id = %s AND expired_at IS NULL", profile_id
)
853 # Delete the previous profile
854 self
.db
.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
855 WHERE profile_id = %s AND expired_at IS NULL", profile_id
)
857 # Store the new profile
878 """, profile_id
, private_id
, version
, json
.dumps(profile
), country_code
, asn
,
881 def _validate(self
, profile_id
, version
, blob
):
886 raise ValueError("Unsupported profile version")
890 return jsonschema
.validate(blob
, schema
=PROFILE_SCHEMA
)
892 # Raise a ValueError instead which is easier to handle later on
893 except jsonschema
.exceptions
.ValidationError
as e
:
894 raise ValueError("%s" % e
) from e
896 def _preprocess(self
, blob
):
898 Modifies the profile before storing it
900 # Remove the architecture from the release string
901 blob
["system"]["release"]= self
._filter
_release
(blob
["system"]["release"])
905 def _filter_release(self
, release
):
907 Removes the arch part
909 r
= [e
for e
in release
.split() if e
]
911 for s
in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
922 def get_random_profile(self
, when
=None):
924 return self
._get
_profile
("""
944 return self
._get
_profile
("""
957 def get_active_profiles(self
, when
=None):
959 raise NotImplementedError
962 res
= self
.db
.get("""
964 COUNT(*) AS total_profiles,
965 COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
973 return res
.active_profiles
, res
.total_profiles
975 def get_geo_location_map(self
, when
=None):
977 res
= self
.db
.query("""
981 COUNT(*), SUM(COUNT(*)) OVER ()
994 country_code IS NOT NULL
999 res
= self
.db
.query("""
1002 fireinfo_percentage(
1003 COUNT(*), SUM(COUNT(*)) OVER ()
1010 country_code IS NOT NULL
1015 return { row
.country_code
: row
.p
for row
in res
}
1018 def cpu_vendors(self
):
1019 res
= self
.db
.query("""
1021 blob->'cpu'->'vendor' AS vendor
1025 blob->'cpu'->'vendor' IS NOT NULL
1029 return sorted((CPU_VENDORS
.get(row
.vendor
, row
.vendor
) for row
in res
))
1031 def get_cpu_vendors_map(self
, when
=None):
1033 raise NotImplementedError
1036 res
= self
.db
.query("""
1038 blob->'cpu'->'vendor' AS vendor,
1039 fireinfo_percentage(
1040 COUNT(*), SUM(COUNT(*)) OVER ()
1049 blob->'cpu'->'vendor' IS NOT NULL
1051 blob->'cpu'->'vendor'
1054 return { CPU_VENDORS
.get(row
.vendor
, row
.vendor
) : row
.p
for row
in res
}
1056 def get_cpu_flags_map(self
, when
=None):
1058 raise NotImplementedError
1061 res
= self
.db
.query("""
1062 WITH arch_flags AS (
1064 ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
1065 blob->'cpu'->'arch' AS arch,
1066 blob->'cpu'->'flags' AS flags
1072 blob->'cpu'->'arch' IS NOT NULL
1074 blob->'cpu'->'flags' IS NOT NULL
1076 -- Filter out virtual systems
1078 CAST((blob->'system'->'virtual') AS boolean) IS FALSE
1084 fireinfo_percentage(
1090 arch_flags __arch_flags
1092 arch_flags.arch = __arch_flags.arch
1096 arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
1105 result
[row
.arch
][row
.flag
] = row
.p
1107 result
[row
.arch
] = { row
.flag
: row
.p
}
1111 def get_average_memory_amount(self
, when
=None):
1113 res
= self
.db
.get("""
1116 CAST(blob->'system'->'memory' AS numeric)
1130 res
= self
.db
.get("""
1133 CAST(blob->'system'->'memory' AS numeric)
1141 return res
.memory
if res
else 0
1143 def get_arch_map(self
, when
=None):
1145 raise NotImplementedError
1148 res
= self
.db
.query("""
1150 blob->'cpu'->'arch' AS arch,
1151 fireinfo_percentage(
1152 COUNT(*), SUM(COUNT(*)) OVER ()
1159 blob->'cpu'->'arch' IS NOT NULL
1164 return { row
.arch
: row
.p
for row
in res
}
1168 def get_hypervisor_map(self
, when
=None):
1170 raise NotImplementedError
1172 res
= self
.db
.query("""
1174 blob->'hypervisor'->'vendor' AS vendor,
1175 fireinfo_percentage(
1176 COUNT(*), SUM(COUNT(*)) OVER ()
1183 CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1185 blob->'hypervisor'->'vendor' IS NOT NULL
1187 blob->'hypervisor'->'vendor'
1190 return { row
.vendor
: row
.p
for row
in res
}
1192 def get_virtual_ratio(self
, when
=None):
1194 raise NotImplementedError
1197 res
= self
.db
.get("""
1199 fireinfo_percentage(
1201 WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1213 return res
.p
if res
else 0
1217 def get_releases_map(self
, when
=None):
1219 raise NotImplementedError
1222 res
= self
.db
.query("""
1224 blob->'system'->'release' AS release,
1225 fireinfo_percentage(
1226 COUNT(*), SUM(COUNT(*)) OVER ()
1235 blob->'system'->'release' IS NOT NULL
1237 blob->'system'->'release'
1240 return { row
.release
: row
.p
for row
in res
}
1244 def get_kernels_map(self
, when
=None):
1246 raise NotImplementedError
1249 res
= self
.db
.query("""
1251 blob->'system'->'kernel' AS kernel,
1252 fireinfo_percentage(
1253 COUNT(*), SUM(COUNT(*)) OVER ()
1262 blob->'system'->'kernel' IS NOT NULL
1264 blob->'system'->'kernel'
1267 return { row
.kernel
: row
.p
for row
in res
}
1270 "pci" : hwdata
.PCI(),
1271 "usb" : hwdata
.USB(),
1274 def get_vendor_string(self
, subsystem
, vendor_id
):
1276 cls
= self
.subsystem2class
[subsystem
]
1280 return cls
.get_vendor(vendor_id
) or ""
1282 def get_model_string(self
, subsystem
, vendor_id
, model_id
):
1284 cls
= self
.subsystem2class
[subsystem
]
1288 return cls
.get_device(vendor_id
, model_id
) or ""
1290 def get_vendor_list(self
, when
=None):
1292 raise NotImplementedError
1295 res
= self
.db
.query("""
1298 jsonb_array_elements(blob->'devices') AS device
1306 blob->'devices' IS NOT NULL
1308 jsonb_typeof(blob->'devices') = 'array'
1312 devices.device->'subsystem' AS subsystem,
1313 devices.device->'vendor' AS vendor
1317 devices.device->'subsystem' IS NOT NULL
1319 devices.device->'vendor' IS NOT NULL
1321 NOT devices.device->>'driver' = 'usb'
1329 vendor
= self
.get_vendor_string(row
.subsystem
, row
.vendor
) or row
.vendor
1331 # Drop if vendor could not be determined
1336 vendors
[vendor
].append((row
.subsystem
, row
.vendor
))
1338 vendors
[vendor
] = [(row
.subsystem
, row
.vendor
)]
1342 def _get_devices(self
, query
, *args
, **kwargs
):
1343 res
= self
.db
.query(query
, *args
, **kwargs
)
1345 return [Device(self
.backend
, blob
) for blob
in res
]
1347 def get_devices_by_vendor(self
, subsystem
, vendor
, when
=None):
1349 raise NotImplementedError
1352 return self
._get
_devices
("""
1355 jsonb_array_elements(blob->'devices') AS device
1363 blob->'devices' IS NOT NULL
1365 jsonb_typeof(blob->'devices') = 'array'
1376 jsonb_to_record(devices.device) AS device(
1386 devices.device->>'subsystem' = %s
1388 devices.device->>'vendor' = %s
1390 NOT devices.device->>'driver' = 'usb'
1397 """, subsystem
, vendor
,
1400 def get_devices_by_driver(self
, driver
, when
=None):
1402 raise NotImplementedError
1405 return self
._get
_devices
("""
1408 jsonb_array_elements(blob->'devices') AS device
1416 blob->'devices' IS NOT NULL
1418 jsonb_typeof(blob->'devices') = 'array'
1429 jsonb_to_record(devices.device) AS device(
1439 devices.device->>'driver' = '%s'