]>
git.ipfire.org Git - ipfire.org.git/blob - src/backend/fireinfo.py
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}$",
109 "type" : ["integer", "null"],
115 "pattern" : r
"^.{,24}$",
119 "type" : ["integer", "null"],
122 "type" : ["string", "null"],
123 "pattern" : r
"^.{,80}$",
129 "type" : ["integer", "null"],
133 "pattern" : r
"^.{,80}$",
136 "additionalProperties" : False,
157 "type" : ["string", "null"],
158 "pattern" : r
"^.{,20}$",
161 "type" : ["string", "null"],
162 "pattern" : r
"^.{,24}$",
166 "pattern" : r
"^[a-z0-9]{4}$",
169 "type" : ["string", "null"],
170 "pattern" : r
"^[a-z0-9]{4}$",
173 "type" : ["string", "null"],
174 "pattern" : r
"^[a-z0-9]{4}$",
178 "pattern" : r
"^[a-z]{3}$",
181 "type" : ["string", "null"],
182 "pattern" : r
"^[a-z0-9]{4}$",
185 "additionalProperties" : False,
213 "additionalProperties" : False,
222 "pattern" : r
"^.{,40}$",
226 "pattern" : r
"^[a-z]{2}(\.utf8)?$",
232 "type" : ["string", "null"],
233 "pattern" : r
"^.{,80}$",
237 "pattern" : r
"^.{,80}$",
240 "type" : ["number", "null"],
243 "type" : ["string", "null"],
244 "pattern" : r
"^.{,80}$",
250 "additionalProperties" : False,
269 "pattern" : r
"^.{,40}$",
272 "additionalProperties" : False,
283 "additionalProperties" : False,
292 class Network(Object
):
293 def init(self
, blob
):
299 for zone
in ("red", "green", "orange", "blue"):
300 if self
.has_zone(zone
):
305 def has_zone(self
, name
):
306 return self
.blob
.get(name
, False)
310 return self
.has_zone("red")
314 return self
.has_zone("green")
317 def has_orange(self
):
318 return self
.has_zone("orange")
322 return self
.has_zone("blue")
325 class Processor(Object
):
326 def init(self
, blob
):
332 if self
.model_string
and not self
.model_string
.startswith(self
.vendor
):
333 s
.append(self
.vendor
)
336 s
.append(self
.model_string
or "Generic")
338 if self
.core_count
> 1:
339 s
.append("x%s" % self
.core_count
)
345 return self
.blob
.get("arch")
349 vendor
= self
.blob
.get("vendor")
352 return CPU_VENDORS
[vendor
]
358 return self
.blob
.get("family")
362 return self
.blob
.get("model")
366 return self
.blob
.get("stepping")
369 def model_string(self
):
370 return self
.blob
.get("model_string")
374 return self
.blob
.get("flags")
376 def has_flag(self
, flag
):
377 return flag
in self
.flags
380 if self
.vendor
== "Intel" and self
.family
== 6 and self
.model
in (15, 55, 76, 77):
383 return self
.has_flag("ht")
386 def core_count(self
):
387 return self
.blob
.get("count", 1)
392 return self
.core_count
// 2
394 return self
.core_count
397 def clock_speed(self
):
398 return self
.blob
.get("speed", 0)
400 def format_clock_speed(self
):
401 if not self
.clock_speed
:
404 if self
.clock_speed
< 1000:
405 return "%dMHz" % self
.clock_speed
407 return "%.2fGHz" % round(self
.clock_speed
/ 1000, 2)
411 return self
.__bogomips
414 def capabilities(self
):
416 ("64bit", self
.has_flag("lm")),
417 ("aes", self
.has_flag("aes")),
418 ("nx", self
.has_flag("nx")),
419 ("pae", self
.has_flag("pae") or self
.has_flag("lpae")),
420 ("rdrand", self
.has_flag("rdrand")),
423 # If the system is already running in a virtual environment,
424 # we cannot correctly detect if the CPU supports svm or vmx
425 if self
.has_flag("hypervisor"):
426 caps
.append(("virt", None))
428 caps
.append(("virt", self
.has_flag("vmx") or self
.has_flag("svm")))
432 def format_model(self
):
433 s
= self
.model_string
or ""
435 # Remove everything after the @: Intel(R) Core(TM) i7-3770 CPU @ 3.40GHz
436 s
, sep
, rest
= s
.partition("@")
438 for pattern
, repl
in CPU_STRINGS
:
439 if re
.match(pattern
, s
) is None:
442 s
= re
.sub(pattern
, repl
, s
)
445 # Otherwise remove the symbols
446 for i
in ("C", "R", "TM", "tm"):
447 s
= s
.replace("(%s)" % i
, "")
449 # Replace too long strings with shorter ones
451 ("Quad-Core Processor", ""),
452 ("Dual-Core Processor", ""),
453 ("Processor", "CPU"),
454 ("processor", "CPU"),
459 # Remove too many spaces
460 s
= " ".join((e
for e
in s
.split() if e
))
465 def friendly_string(self
):
468 model
= self
.format_model()
472 clock_speed
= self
.format_clock_speed()
474 s
.append("@ %s" % clock_speed
)
477 s
.append("x%s" % self
.count
)
482 class Device(Object
):
485 "00" : N_("Unclassified"),
486 "01" : N_("Mass storage"),
487 "02" : N_("Network"),
488 "03" : N_("Display"),
489 "04" : N_("Multimedia"),
490 "05" : N_("Memory controller"),
492 "07" : N_("Communication"),
493 "08" : N_("Generic system peripheral"),
494 "09" : N_("Input device"),
495 "0a" : N_("Docking station"),
496 "0b" : N_("Processor"),
497 "0c" : N_("Serial bus"),
498 "0d" : N_("Wireless"),
499 "0e" : N_("Intelligent controller"),
500 "0f" : N_("Satellite communications controller"),
501 "10" : N_("Encryption"),
502 "11" : N_("Signal processing controller"),
503 "ff" : N_("Unassigned class"),
507 "00" : N_("Unclassified"),
508 "01" : N_("Multimedia"),
509 "02" : N_("Communication"),
510 "03" : N_("Input device"),
511 "05" : N_("Generic system peripheral"),
513 "07" : N_("Printer"),
514 "08" : N_("Mass storage"),
516 "0a" : N_("Communication"),
517 "0b" : N_("Smart card"),
518 "0d" : N_("Encryption"),
519 "0e" : N_("Display"),
520 "0f" : N_("Personal Healthcare"),
521 "dc" : N_("Diagnostic Device"),
522 "e0" : N_("Wireless"),
523 "ef" : N_("Unclassified"),
524 "fe" : N_("Unclassified"),
525 "ff" : N_("Unclassified"),
529 def init(self
, blob
):
533 return "<%s vendor=%s model=%s>" % (self
.__class
__.__name
__,
534 self
.vendor_string
, self
.model_string
)
536 def __eq__(self
, other
):
537 if isinstance(other
, self
.__class
__):
538 return self
.blob
== other
.blob
540 return NotImplemented
542 def __lt__(self
, other
):
543 if isinstance(other
, self
.__class
__):
544 return self
.cls
< other
.cls
or \
545 self
.vendor_string
< other
.vendor_string
or \
546 self
.vendor
< other
.vendor
or \
547 self
.model_string
< other
.model_string
or \
548 self
.model
< other
.model
550 return NotImplemented
552 def is_showable(self
):
553 if self
.driver
in ("usb", "pcieport", "hub"):
560 return self
.blob
.get("subsystem")
564 return self
.blob
.get("model")
567 def model_string(self
):
568 return self
.fireinfo
.get_model_string(self
.subsystem
, self
.vendor
, self
.model
)
572 return self
.blob
.get("vendor")
575 def vendor_string(self
):
576 return self
.fireinfo
.get_vendor_string(self
.subsystem
, self
.vendor
)
580 return self
.blob
.get("driver")
584 classid
= self
.blob
.get("deviceclass")
586 if self
.subsystem
== "pci":
587 classid
= classid
[:-4]
588 if len(classid
) == 1:
589 classid
= "0%s" % classid
591 elif self
.subsystem
== "usb" and classid
:
592 classid
= classid
.split("/")[0]
593 classid
= "%02x" % int(classid
)
596 return self
.classid2name
[self
.subsystem
][classid
]
601 class System(Object
):
602 def init(self
, blob
):
607 return self
.blob
.get("language")
611 return self
.blob
.get("vendor")
615 return self
.blob
.get("model")
619 return self
.blob
.get("release")
625 return self
.blob
.get("memory") * 1024
628 def friendly_memory(self
):
629 return util
.format_size(self
.memory
or 0)
633 return self
.blob
.get("storage_size", 0)
635 def is_virtual(self
):
636 return self
.blob
.get("virtual", False)
639 class Hypervisor(Object
):
640 def init(self
, blob
):
648 return self
.blob
.get("vendor")
651 class Profile(Object
):
652 def init(self
, profile_id
, private_id
, created_at
, expired_at
, version
, blob
,
653 last_updated_at
, country_code
, **kwargs
):
654 self
.profile_id
= profile_id
655 self
.private_id
= private_id
656 self
.created_at
= created_at
657 self
.expired_at
= expired_at
658 self
.version
= version
660 self
.last_updated_at
= last_updated_at
661 self
.country_code
= country_code
664 return "<%s %s>" % (self
.__class
__.__name
__, self
.profile_id
)
666 def is_showable(self
):
667 return True if self
.blob
else False
672 An alias for the profile ID
674 return self
.profile_id
680 return self
.country_code
683 def location_string(self
):
684 return self
.backend
.get_country_name(self
.location
) or self
.location
690 return [Device(self
.backend
, blob
) for blob
in self
.blob
.get("devices", [])]
696 return System(self
.backend
, self
.blob
.get("system", {}))
702 return Processor(self
.backend
, self
.blob
.get("cpu", {}))
706 def is_virtual(self
):
707 return self
.system
.is_virtual()
710 def hypervisor(self
):
711 return Hypervisor(self
.backend
, self
.blob
.get("hypervisor"))
717 return Network(self
.backend
, self
.blob
.get("network", {}))
720 class Fireinfo(Object
):
721 async def expire(self
):
723 Called to expire any profiles that have not been updated in a fortnight
725 self
.db
.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
726 WHERE last_updated_at <= CURRENT_TIMESTAMP - %s", datetime
.timedelta(days
=14))
728 def _get_profile(self
, query
, *args
, **kwargs
):
729 res
= self
.db
.get(query
, *args
, **kwargs
)
732 return Profile(self
.backend
, **res
)
734 def get_profile_count(self
, when
=None):
736 res
= self
.db
.get("""
751 res
= self
.db
.get("""
761 return res
.count
if res
else 0
765 def get_profile(self
, profile_id
, when
=None):
767 return self
._get
_profile
("""
775 %s BETWEEN created_at AND expired_at
779 return self
._get
_profile
("""
793 def handle_profile(self
, profile_id
, blob
, country_code
=None, asn
=None, when
=None):
794 private_id
= blob
.get("private_id", None)
797 now
= datetime
.datetime
.utcnow()
799 # Fetch the profile version
800 version
= blob
.get("profile_version")
802 # Extract the profile
803 profile
= blob
.get("profile")
806 # Validate the profile
807 self
._validate
(profile_id
, version
, profile
)
809 # Pre-process the profile
810 profile
= self
._preprocess
(profile
)
812 # Fetch the previous profile
813 prev
= self
.get_profile(profile_id
)
816 # Check if the private ID matches
817 if not prev
.private_id
== private_id
:
818 logging
.error("Private ID for profile %s does not match" % profile_id
)
821 # Check when the last update was
822 elif now
- prev
.last_updated_at
< datetime
.timedelta(hours
=6):
823 logging
.warning("Profile %s has been updated too soon" % profile_id
)
826 # Check if the profile has changed
827 elif prev
.version
== version
and prev
.blob
== blob
:
828 logging
.debug("Profile %s has not changed" % profile_id
)
830 # Update the timestamp
831 self
.db
.execute("UPDATE fireinfo SET last_updated_at = CURRENT_TIMESTAMP \
832 WHERE profile_id = %s AND expired_at IS NULL", profile_id
)
836 # Delete the previous profile
837 self
.db
.execute("UPDATE fireinfo SET expired_at = CURRENT_TIMESTAMP \
838 WHERE profile_id = %s AND expired_at IS NULL", profile_id
)
840 # Serialise the profile
842 profile
= json
.dumps(profile
)
844 # Store the new profile
865 """, profile_id
, private_id
, version
, profile
, country_code
, asn
,
868 def _validate(self
, profile_id
, version
, blob
):
873 raise ValueError("Unsupported profile version")
877 return jsonschema
.validate(blob
, schema
=PROFILE_SCHEMA
)
879 # Raise a ValueError instead which is easier to handle later on
880 except jsonschema
.exceptions
.ValidationError
as e
:
881 raise ValueError("%s" % e
) from e
883 def _preprocess(self
, blob
):
885 Modifies the profile before storing it
887 # Remove the architecture from the release string
888 blob
["system"]["release"]= self
._filter
_release
(blob
["system"]["release"])
892 def _filter_release(self
, release
):
894 Removes the arch part
896 r
= [e
for e
in release
.split() if e
]
898 for s
in ("(x86_64)", "(aarch64)", "(i586)", "(armv6l)", "(armv5tel)", "(riscv64)"):
909 def get_random_profile(self
, when
=None):
911 return self
._get
_profile
("""
933 return self
._get
_profile
("""
948 def get_active_profiles(self
, when
=None):
950 raise NotImplementedError
953 res
= self
.db
.get("""
955 COUNT(*) AS total_profiles,
956 COUNT(*) FILTER (WHERE blob IS NOT NULL) AS active_profiles
964 return res
.active_profiles
, res
.total_profiles
966 def get_geo_location_map(self
, when
=None):
968 res
= self
.db
.query("""
972 COUNT(*), SUM(COUNT(*)) OVER ()
985 country_code IS NOT NULL
990 res
= self
.db
.query("""
994 COUNT(*), SUM(COUNT(*)) OVER ()
1001 country_code IS NOT NULL
1006 return { row
.country_code
: row
.p
for row
in res
}
1008 def get_asn_map(self
, when
=None):
1010 res
= self
.db
.query("""
1013 fireinfo_percentage(
1014 COUNT(*), SUM(COUNT(*)) OVER ()
1033 res
= self
.db
.query("""
1036 fireinfo_percentage(
1037 COUNT(*), SUM(COUNT(*)) OVER ()
1050 return { self
.backend
.location
.get_as(row
.asn
) : (row
.c
, row
.p
) for row
in res
}
1053 def cpu_vendors(self
):
1054 res
= self
.db
.query("""
1056 blob->'cpu'->'vendor' AS vendor
1060 blob->'cpu'->'vendor' IS NOT NULL
1064 return sorted((CPU_VENDORS
.get(row
.vendor
, row
.vendor
) for row
in res
))
1066 def get_cpu_vendors_map(self
, when
=None):
1068 raise NotImplementedError
1071 res
= self
.db
.query("""
1073 NULLIF(blob->'cpu'->'vendor', '""'::jsonb) AS vendor,
1074 fireinfo_percentage(
1075 COUNT(*), SUM(COUNT(*)) OVER ()
1084 NULLIF(blob->'cpu'->'vendor', '""'::jsonb)
1087 return { CPU_VENDORS
.get(row
.vendor
, row
.vendor
) : row
.p
for row
in res
}
1089 def get_cpu_flags_map(self
, when
=None):
1091 raise NotImplementedError
1094 res
= self
.db
.query("""
1095 WITH arch_flags AS (
1097 ROW_NUMBER() OVER (PARTITION BY blob->'cpu'->'arch') AS id,
1098 blob->'cpu'->'arch' AS arch,
1099 blob->'cpu'->'flags' AS flags
1105 blob->'cpu'->'arch' IS NOT NULL
1107 blob->'cpu'->'flags' IS NOT NULL
1109 -- Filter out virtual systems
1111 CAST((blob->'system'->'virtual') AS boolean) IS FALSE
1117 fireinfo_percentage(
1123 arch_flags __arch_flags
1125 arch_flags.arch = __arch_flags.arch
1129 arch_flags, jsonb_array_elements(arch_flags.flags) AS flag
1138 result
[row
.arch
][row
.flag
] = row
.p
1140 result
[row
.arch
] = { row
.flag
: row
.p
}
1144 def get_average_memory_amount(self
, when
=None):
1146 res
= self
.db
.get("""
1149 CAST(blob->'system'->'memory' AS numeric)
1163 res
= self
.db
.get("""
1166 CAST(blob->'system'->'memory' AS numeric)
1174 return res
.memory
if res
else 0
1176 def get_arch_map(self
, when
=None):
1178 raise NotImplementedError
1181 res
= self
.db
.query("""
1183 blob->'cpu'->'arch' AS arch,
1184 fireinfo_percentage(
1185 COUNT(*), SUM(COUNT(*)) OVER ()
1192 blob->'cpu'->'arch' IS NOT NULL
1197 return { row
.arch
: row
.p
for row
in res
}
1201 def get_hypervisor_map(self
, when
=None):
1203 raise NotImplementedError
1205 res
= self
.db
.query("""
1207 blob->'hypervisor'->'vendor' AS vendor,
1208 fireinfo_percentage(
1209 COUNT(*), SUM(COUNT(*)) OVER ()
1216 CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1218 blob->'hypervisor'->'vendor' IS NOT NULL
1220 blob->'hypervisor'->'vendor'
1223 return { row
.vendor
: row
.p
for row
in res
}
1225 def get_virtual_ratio(self
, when
=None):
1227 raise NotImplementedError
1230 res
= self
.db
.get("""
1232 fireinfo_percentage(
1234 WHERE CAST((blob->'system'->'virtual') AS boolean) IS TRUE
1246 return res
.p
if res
else 0
1250 def get_releases_map(self
, when
=None):
1252 raise NotImplementedError
1255 res
= self
.db
.query("""
1257 blob->'system'->'release' AS release,
1258 fireinfo_percentage(
1259 COUNT(*), SUM(COUNT(*)) OVER ()
1268 blob->'system'->'release' IS NOT NULL
1270 blob->'system'->'release'
1273 return { row
.release
: row
.p
for row
in res
}
1277 def get_kernels_map(self
, when
=None):
1279 raise NotImplementedError
1282 res
= self
.db
.query("""
1285 blob->'system'->'kernel_release',
1286 blob->'system'->'kernel'
1288 fireinfo_percentage(
1289 COUNT(*), SUM(COUNT(*)) OVER ()
1299 blob->'system'->'kernel_release' IS NOT NULL
1301 blob->'system'->'kernel' IS NOT NULL
1305 blob->'system'->'kernel_release',
1306 blob->'system'->'kernel'
1310 return { row
.kernel
: row
.p
for row
in res
}
1313 "pci" : hwdata
.PCI(),
1314 "usb" : hwdata
.USB(),
1317 def get_vendor_string(self
, subsystem
, vendor_id
):
1319 cls
= self
.subsystem2class
[subsystem
]
1323 return cls
.get_vendor(vendor_id
) or ""
1325 def get_model_string(self
, subsystem
, vendor_id
, model_id
):
1327 cls
= self
.subsystem2class
[subsystem
]
1331 return cls
.get_device(vendor_id
, model_id
) or ""
1333 def get_vendor_list(self
, when
=None):
1335 raise NotImplementedError
1338 res
= self
.db
.query("""
1341 jsonb_array_elements(blob->'devices') AS device
1349 blob->'devices' IS NOT NULL
1351 jsonb_typeof(blob->'devices') = 'array'
1355 devices.device->'subsystem' AS subsystem,
1356 devices.device->'vendor' AS vendor
1360 devices.device->'subsystem' IS NOT NULL
1362 devices.device->'vendor' IS NOT NULL
1364 NOT devices.device->>'driver' = 'usb'
1372 vendor
= self
.get_vendor_string(row
.subsystem
, row
.vendor
) or row
.vendor
1374 # Drop if vendor could not be determined
1379 vendors
[vendor
].append((row
.subsystem
, row
.vendor
))
1381 vendors
[vendor
] = [(row
.subsystem
, row
.vendor
)]
1385 def _get_devices(self
, query
, *args
, **kwargs
):
1386 res
= self
.db
.query(query
, *args
, **kwargs
)
1388 return [Device(self
.backend
, blob
) for blob
in res
]
1390 def get_devices_by_vendor(self
, subsystem
, vendor
, when
=None):
1392 raise NotImplementedError
1395 return self
._get
_devices
("""
1398 jsonb_array_elements(blob->'devices') AS device
1406 blob->'devices' IS NOT NULL
1408 jsonb_typeof(blob->'devices') = 'array'
1419 jsonb_to_record(devices.device) AS device(
1429 devices.device->>'subsystem' = %s
1431 devices.device->>'vendor' = %s
1433 NOT devices.device->>'driver' = 'usb'
1440 """, subsystem
, vendor
,
1443 def get_devices_by_driver(self
, driver
, when
=None):
1445 raise NotImplementedError
1448 return self
._get
_devices
("""
1451 jsonb_array_elements(blob->'devices') AS device
1459 blob->'devices' IS NOT NULL
1461 jsonb_typeof(blob->'devices') = 'array'
1472 jsonb_to_record(devices.device) AS device(
1482 devices.device->>'driver' = %s