mirror of https://github.com/containers/qm.git
Compare commits
730 Commits
Author | SHA1 | Date |
---|---|---|
|
c9868e8507 | |
|
5938fa91fe | |
|
3c960da3cc | |
|
1e6111f2d4 | |
|
c84b01d598 | |
|
08fb1c1a66 | |
|
d3cd2a04e4 | |
|
6ad8cb5ff4 | |
|
da31efb4d8 | |
|
595be738b9 | |
|
257e73d65d | |
|
8f137784f2 | |
|
f0b8127906 | |
|
fab260d36c | |
|
c063b36fbb | |
|
034378e7c8 | |
|
9f2801ec0f | |
|
a633389b5c | |
|
4efd62c237 | |
|
13a16718bf | |
|
70ce24df95 | |
|
ceebe725e6 | |
|
bfcd6bcc20 | |
|
4d74d3aaa6 | |
|
217f23c065 | |
|
1df8743403 | |
|
e9e0d962bb | |
|
a8575fc136 | |
|
1c0350960d | |
|
85a8640c15 | |
|
0bea8fa121 | |
|
6dd6ec4fa4 | |
|
19c6d30934 | |
|
d3e4209241 | |
|
c4f1ed9cf0 | |
|
7d9bc52e5c | |
|
5c4aa9546d | |
|
dcd89dddbe | |
|
72e7f5adc8 | |
|
648f6416ca | |
|
3923c628f1 | |
|
f5c47e2bee | |
|
1758aab6a3 | |
|
5e98e5826f | |
|
fb6116c5cf | |
|
2e70fe16d3 | |
|
9f2b3fa700 | |
|
daec692be0 | |
|
647e56c900 | |
|
3ab116d733 | |
|
f4314581b1 | |
|
4fbfebc24b | |
|
b4b399c927 | |
|
10c87d916d | |
|
2ca6668b12 | |
|
4b3d2dfc61 | |
|
0f170e8967 | |
|
45f3e6e569 | |
|
4eba902d46 | |
|
8b53cea2ae | |
|
018318c844 | |
|
735a8dc733 | |
|
a55521851e | |
|
bdaec559e3 | |
|
8b91f7b631 | |
|
5095f24b49 | |
|
279289e581 | |
|
974f65b506 | |
|
893417b4b6 | |
|
a30be64c2c | |
|
7ca5b38338 | |
|
5c1e4202d5 | |
|
13c8a2c88d | |
|
8ee206668c | |
|
d96a3f587c | |
|
69ac992063 | |
|
80f8746fa7 | |
|
f50555cfd7 | |
|
a6e289ffe0 | |
|
122677c853 | |
|
738632738f | |
|
d02de8f746 | |
|
49637001c6 | |
|
1fa5ddfb83 | |
|
eb9016d8b0 | |
|
1280331fbc | |
|
56f0b0f968 | |
|
e8eb967ccc | |
|
c91df0bc1e | |
|
81d79a4ec7 | |
|
c5da37a8c5 | |
|
9db5cde521 | |
|
3a95050279 | |
|
f6cb3efadc | |
|
8f990c7f24 | |
|
5ba1055155 | |
|
5301363274 | |
|
b7d0eda13d | |
|
27cfd18e54 | |
|
e7b369c43c | |
|
949029be51 | |
|
e6c664db85 | |
|
10585de109 | |
|
d2c835982b | |
|
6f3bf2add4 | |
|
a0ddb2dd81 | |
|
8ec51ba47e | |
|
9d814fc6e7 | |
|
eb7b9229ea | |
|
20542d7279 | |
|
9b2f82970e | |
|
74a169bc9c | |
|
f2f13569c0 | |
|
7a423be4d4 | |
|
22e22781e4 | |
|
3a27c8b4ed | |
|
157a7843f0 | |
|
2229e960c4 | |
|
b581ff71cc | |
|
cb6bb2cdf3 | |
|
11aef5ccaa | |
|
558cc12c15 | |
|
7be55d6ffa | |
|
6558b5b63a | |
|
59bc58b38e | |
|
21c9895b25 | |
|
d4a5732ebd | |
|
c74db387c2 | |
|
38374ff2c7 | |
|
6e15f6cba5 | |
|
05cfa40e7e | |
|
c95f521942 | |
|
950f07d91b | |
|
a09ba109d9 | |
|
2e485b84f7 | |
|
b7264e4c15 | |
|
eda62c8931 | |
|
caa22ea148 | |
|
6666250f0f | |
|
1729549b42 | |
|
a7966f2fa1 | |
|
16a3ca7cbd | |
|
e67a9090e0 | |
|
071586d647 | |
|
1c53e0f14c | |
|
9950dd1af0 | |
|
27d33364ce | |
|
236ad09377 | |
|
5d71cc01ed | |
|
837517ce1e | |
|
6b63a11d7c | |
|
009b5a4f66 | |
|
acda4fabd3 | |
|
0ccc4695be | |
|
4f3a2eb170 | |
|
2aa35bb6fc | |
|
6459e7eca8 | |
|
73b5a9062d | |
|
dc8e8b8e4c | |
|
ff43278404 | |
|
6bab941c56 | |
|
ab4567c918 | |
|
8cfb3b6549 | |
|
d6b4c7acee | |
|
07132460c8 | |
|
91de011371 | |
|
488f2ea18e | |
|
dc06790fd5 | |
|
5935d88e9d | |
|
863ca5d523 | |
|
a19d593165 | |
|
0ef4f3d37e | |
|
3eba152036 | |
|
c3e3a47b1a | |
|
ef5463d68b | |
|
d17888b7e8 | |
|
97d3ad1f8a | |
|
4359aec149 | |
|
4e3252d319 | |
|
8f4d5cabb3 | |
|
6b6db9ddde | |
|
07824ae976 | |
|
8fdc8c3d4a | |
|
78cea34772 | |
|
ab493daf2e | |
|
0b38e6e3bb | |
|
93e3f51fa5 | |
|
cac73007b2 | |
|
3ae7b49c64 | |
|
14829855f3 | |
|
4a9bec30b4 | |
|
4551d9b33d | |
|
637417ef65 | |
|
3b907fc1ed | |
|
d5c11a7793 | |
|
7ae21417a3 | |
|
9e0550ebb2 | |
|
7d48592ee5 | |
|
da70aa5980 | |
|
1b6a02a7de | |
|
e573bccee0 | |
|
e5bc702528 | |
|
e2ad952464 | |
|
9f4d78bc0e | |
|
cc031d0454 | |
|
d483f5e4b5 | |
|
b4635b1901 | |
|
5826e1c758 | |
|
c409c8bde5 | |
|
848b5f4a9a | |
|
1601fce179 | |
|
2f8ed3a870 | |
|
d147a9c47a | |
|
561050f0bd | |
|
22c2c00872 | |
|
6f4636ce15 | |
|
aa32b1d9a4 | |
|
5b6b21df2a | |
|
ce35fd9fed | |
|
a194cd2160 | |
|
2cb6d3e9bd | |
|
a1c7f8d5c6 | |
|
e382fa9fe4 | |
|
d8b47a3a27 | |
|
43e0c7f245 | |
|
cbe21d6fb5 | |
|
4feb349982 | |
|
1cd84ffe8e | |
|
8011840cc2 | |
|
ba373d0068 | |
|
206e99d4a6 | |
|
76cf0c7f21 | |
|
10578f6641 | |
|
068c26df3c | |
|
077c2bb28d | |
|
d97d144471 | |
|
f58c315bef | |
|
c49711eefd | |
|
26cbbb5cb4 | |
|
93e540ea92 | |
|
84e090c10d | |
|
645c4d7ee8 | |
|
049d022ff1 | |
|
3c2b51498f | |
|
cd2365f73e | |
|
dbc8d50b1b | |
|
df4232c11a | |
|
d60ede6de7 | |
|
b8b3e1a0a9 | |
|
2ab9dbeee6 | |
|
4d9e753105 | |
|
402a3701f4 | |
|
26ba84b3aa | |
|
67a9d9a876 | |
|
9b0bbf09d8 | |
|
ed4d501c1f | |
|
c3e66f1a0d | |
|
e2af86948b | |
|
a5c3abb605 | |
|
79ce2222d9 | |
|
a0a1cacac5 | |
|
1447246512 | |
|
a275ad45c2 | |
|
1851e19668 | |
|
be96b80706 | |
|
f61b5c5cf1 | |
|
49da5e2403 | |
|
eace5c70b5 | |
|
b3d8f40f44 | |
|
d0cf966934 | |
|
513167c208 | |
|
2de345e6d5 | |
|
74529a28dc | |
|
756947df06 | |
|
6b2a7e9d58 | |
|
515a26ee6d | |
|
64fc09a13b | |
|
dbc6c64e54 | |
|
dbe7d25606 | |
|
96ff300807 | |
|
a69551f97f | |
|
0c0e8a7460 | |
|
9d4c953cb4 | |
|
46c8a966e0 | |
|
6ff1538d59 | |
|
270e21ddf7 | |
|
3000572a0f | |
|
7e4d52acfc | |
|
33b7df839f | |
|
4fe14203dd | |
|
326aacf4c9 | |
|
1c969c8c49 | |
|
32bb6d35a8 | |
|
282f04da53 | |
|
6640439547 | |
|
8b7ff62ff2 | |
|
ad4e074d26 | |
|
88883751db | |
|
37987e0a37 | |
|
615ffcbf07 | |
|
9b8708755a | |
|
71d0f8527c | |
|
974d764550 | |
|
acf9b5577d | |
|
0f3e0ccd6d | |
|
8252785c78 | |
|
cfdfbf097a | |
|
7d08fe433e | |
|
24b9f4c086 | |
|
f89236eaad | |
|
6ae66a8a3f | |
|
b5efe519b3 | |
|
5fe57718e1 | |
|
aeb7478ad0 | |
|
0322896f46 | |
|
f1ef96872c | |
|
3792942f43 | |
|
4dfad9c64d | |
|
8c509144d1 | |
|
ae27595536 | |
|
85a8e5b5aa | |
|
2392290295 | |
|
6e6269d2e0 | |
|
e1df9b48c2 | |
|
7856625a07 | |
|
99a00fb014 | |
|
a05454f321 | |
|
9ba9ddebf6 | |
|
7734f9b162 | |
|
d3044cc32d | |
|
72a12c58c6 | |
|
64c9bc7952 | |
|
db67f30bd6 | |
|
0bf596eaa3 | |
|
70650d16a8 | |
|
edbd6ea940 | |
|
fdabe3fa08 | |
|
ce6a9cf200 | |
|
e2c84c1cb3 | |
|
f53bee5cc0 | |
|
460c4b524b | |
|
d6cf03bb00 | |
|
6bd9cddf0b | |
|
448f8a380a | |
|
5e5c892332 | |
|
7f03a18757 | |
|
e44b2ea89f | |
|
e776b73854 | |
|
693fde1636 | |
|
ef80541768 | |
|
62e86dd2ec | |
|
4368f13613 | |
|
0068cea5f3 | |
|
0679d5995a | |
|
90d29cf9a1 | |
|
3905d41f64 | |
|
62c9da7c56 | |
|
16ba98d29a | |
|
b9d6dddd9e | |
|
23489208a8 | |
|
783fe7a78e | |
|
a7fbb63591 | |
|
4b8f2f09fc | |
|
b403404aeb | |
|
ef71ff9aeb | |
|
e26a131d78 | |
|
6a4c9fdccc | |
|
8275fc7387 | |
|
7cad7f8901 | |
|
9b890599d8 | |
|
d2ac2b3720 | |
|
0455dda5b5 | |
|
19c363c739 | |
|
8f3439b2b5 | |
|
27a35df239 | |
|
48fa529ea0 | |
|
0a2eb661c9 | |
|
069d85467b | |
|
6f5264d2bc | |
|
b4b76738a1 | |
|
9539498ab9 | |
|
5d2c402181 | |
|
3d491af0c8 | |
|
5166d3d210 | |
|
ca075b9329 | |
|
aff4a4a196 | |
|
3a8e56a481 | |
|
d5a46689a2 | |
|
a63db527a5 | |
|
2e577bd399 | |
|
bc87f9ed4e | |
|
8ada10507e | |
|
c43b56f8c5 | |
|
904b572d4b | |
|
ff4a438f06 | |
|
1f2cd69ac0 | |
|
4706e90fc1 | |
|
269026aa95 | |
|
bc3d5dceb1 | |
|
c078c50f2b | |
|
2ca0507d37 | |
|
78b899d4fb | |
|
af93c7fbbc | |
|
4c73047cfb | |
|
99ce67561f | |
|
5d67b751dc | |
|
fa0cae7bb4 | |
|
d25da43960 | |
|
625113cb47 | |
|
3db90a7bdf | |
|
a880645018 | |
|
787dba469c | |
|
7e9e73bec9 | |
|
528db8233e | |
|
f19543f8f4 | |
|
b81f841167 | |
|
0cac7d20f8 | |
|
b7f7cb8d2b | |
|
1eb55c442b | |
|
38ce17f331 | |
|
6f58188335 | |
|
70eb93e038 | |
|
81e7670442 | |
|
faf2c07921 | |
|
dd311c6b03 | |
|
0c3420a8bd | |
|
a6b2901e39 | |
|
34b25e23b7 | |
|
468132734d | |
|
66c57a78dc | |
|
307dff37ca | |
|
88ce66b119 | |
|
0253bc966b | |
|
f7a05c2faa | |
|
0866878c01 | |
|
4095073b27 | |
|
f58cc875dd | |
|
3f83ae96fd | |
|
0f6c591c0c | |
|
e6f32bdb7d | |
|
c6dab9e529 | |
|
9885efb580 | |
|
e4380f3458 | |
|
aa286fd55a | |
|
40d60dac5f | |
|
5c77640c94 | |
|
068d25da74 | |
|
7c1e4e7708 | |
|
d2e18436d0 | |
|
52bac61d0d | |
|
e4387bc00a | |
|
cddf8f6e0c | |
|
c883d171c2 | |
|
e2f0c0b11f | |
|
cb92d2d958 | |
|
5219a0486a | |
|
e8993744d1 | |
|
6ad3a6bae5 | |
|
3ec382733b | |
|
3019b2ef4d | |
|
e9b3e04b73 | |
|
aa080c3a77 | |
|
0b03bda611 | |
|
ec074093ad | |
|
b9b814b287 | |
|
70350aecdf | |
|
676f1a0a0d | |
|
b810de4125 | |
|
bdc6b1f564 | |
|
219064da9f | |
|
6cd683b7ad | |
|
df395155dd | |
|
ca716924d6 | |
|
e4cf422ecf | |
|
43075c3f2d | |
|
c906ab2a9e | |
|
a3815b5862 | |
|
30de9ff16b | |
|
955e8b539b | |
|
e0198dc1a8 | |
|
ee005daa89 | |
|
b67fe3ea42 | |
|
8a0c66c495 | |
|
f1ee3169b6 | |
|
985b0340f9 | |
|
0b5778c18e | |
|
57fb04cc22 | |
|
24a8580f33 | |
|
c9d57e4592 | |
|
948d09a2e6 | |
|
8afcc1b21d | |
|
bdaf7c00a0 | |
|
2c44560298 | |
|
e5698e3c71 | |
|
bc473f434c | |
|
6fce5754cc | |
|
d7ece83425 | |
|
bbd92d753d | |
|
d4b6d37498 | |
|
6ffbf839cb | |
|
ae9fad3086 | |
|
8a5fb8ca55 | |
|
0e2899c457 | |
|
b2cea2f9e1 | |
|
d694727108 | |
|
9303f9ec8e | |
|
625b391efb | |
|
32f42cca82 | |
|
445d496d8b | |
|
4e32144bbd | |
|
ad256dd525 | |
|
cfbce8d09a | |
|
3167e6ede1 | |
|
27fb454f43 | |
|
5ab7719750 | |
|
151f842910 | |
|
faf67595af | |
|
a378056b9a | |
|
715bbf8cb3 | |
|
6e3d03b6d4 | |
|
39c200eabd | |
|
e0d5da2c36 | |
|
f4f51610a7 | |
|
db85eeb51e | |
|
de37beb5f9 | |
|
e47d560850 | |
|
278bd14dba | |
|
d1e6e0eab2 | |
|
44da37e34c | |
|
230e5d74a3 | |
|
e88fe4ff0b | |
|
ea6b11417a | |
|
5f95e5b9a8 | |
|
48948a3e5d | |
|
1d85b25458 | |
|
94998b55c9 | |
|
1e7bf2a395 | |
|
ed396f59d3 | |
|
c411591101 | |
|
0f4e1332a9 | |
|
608412509f | |
|
e9d8646401 | |
|
b953c4f735 | |
|
603eb93f9b | |
|
0a7c7afd14 | |
|
ad07470515 | |
|
a79218217b | |
|
8c1282bd05 | |
|
31dd114d7c | |
|
d8478d4a12 | |
|
faed96d984 | |
|
55b493d5a8 | |
|
f64e83098a | |
|
e4733b48c4 | |
|
a6920550f8 | |
|
f04d10e033 | |
|
b53424fb76 | |
|
f206278ef6 | |
|
b41dd9a6e2 | |
|
9ef2f789b4 | |
|
252412540b | |
|
dd5bf23f3e | |
|
33e5edd3bb | |
|
2f2a453d4d | |
|
5c9d4ba726 | |
|
f3777694c1 | |
|
c8a00601dc | |
|
fe151fadce | |
|
f48969a9e0 | |
|
8029307582 | |
|
a14d3b8476 | |
|
b0f502c6af | |
|
b4cfb630d3 | |
|
dd9545af1c | |
|
edcd9c7696 | |
|
7d9f9db0b2 | |
|
32eba5752d | |
|
62c1ee9f17 | |
|
a44f687e42 | |
|
befec14909 | |
|
67dfef1ea5 | |
|
2b6279673a | |
|
094501962e | |
|
ac84d0ccf1 | |
|
2895fc021a | |
|
33d07eb065 | |
|
dabcf22b78 | |
|
762b0ef948 | |
|
1532ac0e16 | |
|
e2d0089a98 | |
|
a8ce78aabc | |
|
8c8cce3758 | |
|
7464e9c1eb | |
|
0c5b515914 | |
|
391f67acc6 | |
|
7eb86d7bd0 | |
|
adaec344c6 | |
|
97b6f573ed | |
|
96fb7a7ce5 | |
|
d309aa7611 | |
|
1e18ef145e | |
|
9e2a056224 | |
|
b923838dfd | |
|
a54acf1469 | |
|
958a2d7fea | |
|
83fc1e6335 | |
|
03f2d6c5d4 | |
|
2eb0e10135 | |
|
1cf6b9937d | |
|
4a5cfef43b | |
|
7b38b904ce | |
|
147b041c75 | |
|
6a7af260f1 | |
|
34b9eb0d5f | |
|
3a3a79e5bb | |
|
ec0b33a357 | |
|
addfca3665 | |
|
33825b29d8 | |
|
76c435b0d8 | |
|
817ed77ac0 | |
|
6169713128 | |
|
504e8048c0 | |
|
0484af3d2f | |
|
85b2ffbb9f | |
|
4694d9bdda | |
|
9a12a0268d | |
|
51b3aa04fb | |
|
80e65fed8f | |
|
97a4ce5c65 | |
|
c3332121c5 | |
|
e15da9476a | |
|
f0b2123840 | |
|
6110b1bfea | |
|
68a58a757d | |
|
a223eb58ac | |
|
4b3c95c45b | |
|
5d7dc959c9 | |
|
a503df3cce | |
|
58cfcc0625 | |
|
c293cec773 | |
|
9124842a8a | |
|
27ad740ec0 | |
|
f15955a948 | |
|
53b2511170 | |
|
6ac25aae10 | |
|
bcfeb8828b | |
|
32c88d0575 | |
|
31c279b697 | |
|
e63a1ad603 | |
|
c68c4bab1c | |
|
4a022c7c09 | |
|
a53813d7c3 | |
|
4c6e2d3e9e | |
|
c5ff05fa5c | |
|
da12f5748b | |
|
1e75af08d6 | |
|
b364ec9e2d | |
|
fd0e1c3ec5 | |
|
b8e43e8d5f | |
|
b664416d28 | |
|
fdb2d1ee5e | |
|
1592a66e2f | |
|
84886c0fbf | |
|
8690baa1d0 | |
|
d9f511fb01 | |
|
df1ac5a30c | |
|
ef85266f0c | |
|
0621d9f89b | |
|
c885ec5ac9 | |
|
882b67e470 | |
|
2a5d8fbbfe | |
|
1cfd9786d4 | |
|
3f09c0de91 | |
|
1b7c966e92 | |
|
44bccbc515 | |
|
115890a3df | |
|
778c36cd53 | |
|
683e988643 | |
|
a306ea1b70 | |
|
753fb5c79a | |
|
6df72e53bc | |
|
4d63514f3e | |
|
58deeada21 | |
|
efdb6f73c7 | |
|
52a41c822a | |
|
a9263290d0 | |
|
34fd77860b | |
|
9f53c45e39 | |
|
7680b7a927 | |
|
cbe101fd84 | |
|
d2f45228b1 | |
|
6d5730c1ab | |
|
6bd0c2cddc | |
|
f6b269f6ea | |
|
481093ea84 | |
|
07157cae73 | |
|
013687d21d | |
|
529c1ee8f2 | |
|
3072622f99 | |
|
9bdb8f2eac | |
|
4033ed1f47 | |
|
6976a8931f | |
|
293d379701 | |
|
e5205fd179 | |
|
4d488957e9 | |
|
45e661b077 | |
|
b74d1c707d | |
|
715480aa21 | |
|
2bfa75f1e7 | |
|
ab79770072 | |
|
1d39f9e8df | |
|
ebfcdd1c10 | |
|
971d26e74b | |
|
26fe1780de | |
|
76d86422bc | |
|
5d81bf388e | |
|
a9d01a256f | |
|
1b7f437729 | |
|
477c19ec98 | |
|
1f7d506d9d | |
|
39f7f41d91 | |
|
2285615b8f | |
|
15c886a12c | |
|
b4f182c4f4 | |
|
3bdf984295 | |
|
b9358b49c9 | |
|
71b872a5a0 | |
|
9db6965d1b | |
|
3f81fa3f0f | |
|
4ce04a25bd |
|
@ -0,0 +1 @@
|
|||
1
|
|
@ -0,0 +1,5 @@
|
|||
# See github docs for more info on the syntax:
|
||||
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Default to all maintainers if nothing more specific matches
|
||||
* @rhatdan @dougsland @yarboa @nsednev @aesteve-rh @pengshanyu @kleinffm
|
|
@ -0,0 +1,18 @@
|
|||
name: Check Cgroup Version
|
||||
|
||||
on: [push, pull_request] # Trigger on push or pull request. Adjust as needed.
|
||||
|
||||
jobs:
|
||||
check-cgroup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Check cgroup version
|
||||
run: |
|
||||
echo "Checking cgroup version and settings..."
|
||||
grep cgroup /proc/filesystems
|
||||
mount | grep cgroup
|
||||
cat /proc/cgroups
|
||||
cat /sys/fs/cgroup/cgroup.controllers
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Function to check if a file is a Python script
|
||||
is_python_script() {
|
||||
local file="$1"
|
||||
# Check if the file extension is .py or .python
|
||||
if [[ "$file" == *.py ]]; then
|
||||
return 0 # It's a Python script
|
||||
fi
|
||||
|
||||
# Check if the file starts with a Python shebang
|
||||
if head -n 1 "$file" | grep -qE '^#!/usr/bin/env python(3)?|^#!/usr/bin/python(3)?'; then
|
||||
return 0 # It's a Python script
|
||||
else
|
||||
return 1 # Not a Python script
|
||||
fi
|
||||
}
|
||||
|
||||
# Directory to search (default is current directory)
|
||||
search_dir="${1:-.}"
|
||||
|
||||
# Find all files (excluding directories)
|
||||
find "$search_dir" -type f | while read -r file; do
|
||||
if is_python_script "$file"; then
|
||||
echo "Python script found: $file"
|
||||
# Execute Bandit on the found Python script
|
||||
bandit -r "$file"
|
||||
fi
|
||||
done
|
|
@ -0,0 +1,42 @@
|
|||
name: Build Subpackages
|
||||
|
||||
on:
|
||||
pull_request
|
||||
|
||||
jobs:
|
||||
build-subpackages:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: fedora:latest # Use Fedora as the container image
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo dnf install -y git make rpmdevtools rpmbuild
|
||||
|
||||
|
||||
- name: Run make for each subsystem
|
||||
run: |
|
||||
subsystem_build_failures=()
|
||||
for dir in subsystems/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
subsystem=$(basename "$dir")
|
||||
echo "Running make for $subsystem..."
|
||||
make TARGETS=$subsystem subpackages
|
||||
if [ $? -ne 0 ]; then
|
||||
subsystem_build_failures+=("$subsystem")
|
||||
echo "❌ Make failed for $subsystem" >&2
|
||||
fi
|
||||
fi
|
||||
done
|
||||
if (( ${#subsystem_build_failures[@]} == 0 )); then
|
||||
echo "✅ All subsystems built successfully"; \
|
||||
exit 0;
|
||||
else
|
||||
echo "❌ The following subsystems failed to build: ";
|
||||
echo -e "\t${subsystem_build_failures[@]}" | tr ' ' ', ';
|
||||
exit 1;
|
||||
fi
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
name: Python Security Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
bandit-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install Bandit
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install bandit
|
||||
|
||||
- name: Run Python Security Check script
|
||||
run: |
|
||||
./.github/workflows/check-python-bandit .
|
||||
|
||||
- name: Upload Bandit report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bandit-report
|
||||
path: bandit-output.txt
|
|
@ -0,0 +1,30 @@
|
|||
name: MkDocs Build Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
mkdocs-build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install MkDocs and dependencies
|
||||
run: |
|
||||
pip install \
|
||||
mkdocs \
|
||||
mkdocs-material \
|
||||
pymdown-extensions
|
||||
|
||||
- name: Build MkDocs site
|
||||
run: mkdocs build -f docs/mkdocs.yml --strict
|
|
@ -0,0 +1,47 @@
|
|||
name: OCI Hooks Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'oci-hooks/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'oci-hooks/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: python:3.11-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.21'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y jq
|
||||
pip install tox
|
||||
# Install shfmt for shell script formatting
|
||||
go install mvdan.cc/sh/v3/cmd/shfmt@latest
|
||||
|
||||
- name: Make OCI hook scripts executable
|
||||
run: |
|
||||
chmod +x oci-hooks/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
|
||||
- name: Run code quality checks
|
||||
run: |
|
||||
cd oci-hooks
|
||||
tox -e lint
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
FORCE_MOCK_DEVICES: true
|
||||
run: |
|
||||
cd oci-hooks
|
||||
tox -e all
|
|
@ -0,0 +1,21 @@
|
|||
name: pre-commit-checks
|
||||
|
||||
on:
|
||||
pull_request
|
||||
|
||||
jobs:
|
||||
pre_commit:
|
||||
name: Check code with precommit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run precommit check
|
||||
uses: pre-commit/action@v3.0.1
|
|
@ -1,5 +1,9 @@
|
|||
/*.tar.gz
|
||||
rpmbuild/
|
||||
tmp
|
||||
qm.pp.bz2
|
||||
*~
|
||||
qm_file_contexts
|
||||
*.8
|
||||
tests/e2e/ContainerFile.template
|
||||
__pycache__/
|
||||
|
|
24
.packit.sh
24
.packit.sh
|
@ -6,22 +6,34 @@
|
|||
|
||||
set -eo pipefail
|
||||
|
||||
# Set path to rpm spec file
|
||||
SPEC_FILE=rpm/qm.spec
|
||||
|
||||
# Get Version from HEAD
|
||||
HEAD_VERSION=$(grep '^policy_module' qm.te | sed 's/[^0-9.]//g')
|
||||
|
||||
# Check version consistency in qm.te and VERSION before proceeding
|
||||
if [[ $(cat VERSION) != "${HEAD_VERSION}" ]]; then
|
||||
echo "Inconsistent versions mentioned in VERSION and qm.te files. Investigate!"
|
||||
echo "Aborting Packit tasks!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate source tarball
|
||||
git archive --prefix=qm-$HEAD_VERSION/ -o qm-$HEAD_VERSION.tar.gz HEAD
|
||||
git archive --prefix="qm-${HEAD_VERSION}/" -o "rpm/qm-${HEAD_VERSION}.tar.gz" HEAD
|
||||
|
||||
# RPM Spec modifications
|
||||
|
||||
# Update Version in spec with Version from qm.te
|
||||
sed -i "s/^Version:.*/Version: $HEAD_VERSION/" qm.spec
|
||||
sed -i "s/^Version:.*/Version: ${HEAD_VERSION}/" ${SPEC_FILE}
|
||||
|
||||
# Update Release in spec with Packit's release envvar
|
||||
sed -i "s/^Release:.*/Release: $PACKIT_RPMSPEC_RELEASE%{?dist}/" qm.spec
|
||||
sed -i "s/^Release:.*/Release: ${PACKIT_RPMSPEC_RELEASE}%{?dist}/" ${SPEC_FILE}
|
||||
|
||||
# Update Source tarball name in spec
|
||||
sed -i "s/^Source:.*.tar.gz/Source: %{name}-$HEAD_VERSION.tar.gz/" qm.spec
|
||||
sed -i "s/^Source0:.*.tar.gz/Source0: %{name}-${HEAD_VERSION}.tar.gz/" ${SPEC_FILE}
|
||||
|
||||
# Add update create additional subpackages in spec
|
||||
# Please refer `Let automation create/publish PR sub-packages` of docs/devel/README.md
|
||||
sed -i 's/\(enable_qm_mount_bind_kvm \).*/\11/' ${SPEC_FILE}
|
||||
|
||||
# Update setup macro to use the correct build dir
|
||||
sed -i "s/^%setup.*/%autosetup -Sgit -n %{name}-$HEAD_VERSION/" qm.spec
|
||||
|
|
189
.packit.yaml
189
.packit.yaml
|
@ -2,66 +2,157 @@
|
|||
# See the documentation for more information:
|
||||
# https://packit.dev/docs/configuration/
|
||||
|
||||
# Build targets can be found at:
|
||||
# https://copr.fedorainfracloud.org/coprs/rhcontainerbot/packit-builds/
|
||||
specfile_path: rpm/qm.spec
|
||||
upstream_tag_template: v{version}
|
||||
|
||||
specfile_path: qm.spec
|
||||
srpm_build_deps:
|
||||
- make
|
||||
|
||||
actions:
|
||||
fix-spec-file:
|
||||
- bash .packit.sh
|
||||
|
||||
jobs:
|
||||
- &copr
|
||||
job: copr_build
|
||||
# Run on every PR
|
||||
- job: copr_build
|
||||
trigger: pull_request
|
||||
owner: rhcontainerbot
|
||||
project: packit-builds
|
||||
enable_net: true
|
||||
# x86_64 is assumed by default
|
||||
# qm is noarch so we only need to test on one arch
|
||||
targets:
|
||||
- fedora-rawhide
|
||||
- fedora-38
|
||||
- fedora-37
|
||||
- centos-stream-9
|
||||
srpm_build_deps:
|
||||
- make
|
||||
- rpkg
|
||||
actions:
|
||||
post-upstream-clone:
|
||||
- rpkg spec --outdir ./
|
||||
fix-spec-file:
|
||||
- bash .packit.sh
|
||||
fedora-development: {}
|
||||
fedora-latest: {}
|
||||
epel-9:
|
||||
additional_repos:
|
||||
- copr://@centos-automotive-sig/bluechi-snapshot
|
||||
epel-10:
|
||||
additional_repos:
|
||||
- copr://@centos-automotive-sig/bluechi-snapshot
|
||||
|
||||
- <<: *copr
|
||||
# Run on commit to main branch
|
||||
# Run on commit to main branch
|
||||
- &copr
|
||||
job: copr_build
|
||||
trigger: commit
|
||||
branch: main
|
||||
project: podman-next
|
||||
targets:
|
||||
- fedora-rawhide-aarch64
|
||||
- fedora-rawhide-ppc64le
|
||||
- fedora-rawhide-s390x
|
||||
- fedora-rawhide-x86_64
|
||||
- fedora-38-aarch64
|
||||
- fedora-38-ppc64le
|
||||
- fedora-38-s390x
|
||||
- fedora-38-x86_64
|
||||
- fedora-37-aarch64
|
||||
- fedora-37-ppc64le
|
||||
- fedora-37-s390x
|
||||
- fedora-37-x86_64
|
||||
- centos-stream+epel-next-9-aarch64
|
||||
- centos-stream+epel-next-9-ppc64le
|
||||
- centos-stream+epel-next-9-s390x
|
||||
- centos-stream+epel-next-9-x86_64
|
||||
owner: "@centos-automotive-sig"
|
||||
project: qm-next
|
||||
enable_net: true
|
||||
notifications:
|
||||
failure_comment:
|
||||
message: "QM build failed for merged commit {commit_sha}. Please check logs {logs_url}"
|
||||
|
||||
- <<: *copr
|
||||
# Run on commit to main branch
|
||||
trigger: commit
|
||||
branch: main
|
||||
project: qm
|
||||
targets:
|
||||
- centos-stream-9-aarch64
|
||||
- centos-stream-9-ppc64le
|
||||
- centos-stream-9-s390x
|
||||
- centos-stream-9-x86_64
|
||||
- fedora-stable-aarch64
|
||||
- fedora-stable-ppc64le
|
||||
- fedora-stable-x86_64
|
||||
- epel-9-aarch64
|
||||
- epel-9-ppc64le
|
||||
- epel-9-x86_64
|
||||
- epel-10-aarch64
|
||||
- epel-10-ppc64le
|
||||
- epel-10-x86_64
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
identifier: e2e-multi-bluechi-agents
|
||||
tmt_plan: /plans/e2e/multi-bluechi-agents
|
||||
targets:
|
||||
- epel-9-x86_64
|
||||
manual_trigger: true
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- &bluechi_copr_repo
|
||||
type: repository-file
|
||||
id: https://copr.fedorainfracloud.org/coprs/g/centos-automotive-sig/bluechi-snapshot/repo/centos-stream-9
|
||||
- &bluechi_copr_repo_fedora
|
||||
type: repository-file
|
||||
id: https://copr.fedorainfracloud.org/coprs/g/centos-automotive-sig/bluechi-snapshot/repo/fedora
|
||||
hardware:
|
||||
disk:
|
||||
- size: ">= 20 GB"
|
||||
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
identifier: e2e-ffi
|
||||
tmt_plan: /plans/e2e/ffi
|
||||
targets:
|
||||
- epel-9-x86_64
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- *bluechi_copr_repo
|
||||
tmt:
|
||||
context:
|
||||
scenario : "ffi"
|
||||
hardware:
|
||||
disk:
|
||||
- size: ">= 20 GB"
|
||||
- size: ">= 20 GB"
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
identifier: qm-tier-0
|
||||
tmt_plan: /plans/e2e/tier-0
|
||||
targets:
|
||||
- epel-9-x86_64
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- *bluechi_copr_repo
|
||||
hardware:
|
||||
disk:
|
||||
- size: ">= 20 GB"
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
identifier: kvm-tier-0
|
||||
tmt_plan: /plans/e2e/kvm-tier-0
|
||||
targets:
|
||||
# TBF: fedora-latest -eq 42 no published to public-tf
|
||||
- fedora-41
|
||||
- epel-9-x86_64
|
||||
tf_extra_params:
|
||||
environments:
|
||||
- artifacts:
|
||||
- *bluechi_copr_repo_fedora
|
||||
hardware:
|
||||
disk:
|
||||
- size: ">= 20 GB"
|
||||
virtualization:
|
||||
is-supported: true
|
||||
|
||||
- job: tests
|
||||
trigger: pull_request
|
||||
identifier: automotive-image-builder
|
||||
tmt_plan: /plans/e2e/aib
|
||||
targets:
|
||||
- epel-9-x86_64
|
||||
tf_extra_params:
|
||||
environments:
|
||||
hardware:
|
||||
disk:
|
||||
- size: ">= 20 GB"
|
||||
|
||||
|
||||
- job: propose_downstream
|
||||
trigger: release
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
- epel-9
|
||||
- epel-10
|
||||
|
||||
- job: koji_build
|
||||
trigger: commit
|
||||
dist_git_branches:
|
||||
- fedora-all
|
||||
- epel-9
|
||||
- epel-10
|
||||
|
||||
- job: bodhi_update
|
||||
trigger: commit
|
||||
dist_git_branches:
|
||||
# rawhide updates are created automatically
|
||||
- fedora-branched
|
||||
- epel-9
|
||||
- epel-10
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
args: ["-x", "-a"]
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.41.0
|
||||
hooks:
|
||||
- id: markdownlint
|
||||
args: ["--disable", "MD013", "MD041", "--"]
|
||||
# MD041 disabled because pandoc requires first line to be a metadata block
|
||||
# while markdownlint want it to be a level 1 header
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 7.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: ["-j8", "--ignore=B036,B902"]
|
||||
additional_dependencies: [
|
||||
'flake8-blind-except',
|
||||
'flake8-docstrings',
|
||||
'flake8-bugbear',
|
||||
'flake8-comprehensions',
|
||||
'flake8-docstrings',
|
||||
'flake8-implicit-str-concat',
|
||||
'pydocstyle>=5.0.0',
|
||||
]
|
||||
|
||||
- repo: https://github.com/packit/pre-commit-hooks
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: validate-config
|
|
@ -0,0 +1,6 @@
|
|||
- id: validate-config
|
||||
name: Validate package config
|
||||
description: Check for missing values and incorrect types
|
||||
entry: ./pre-commit-hooks/validate-config.sh
|
||||
language: script
|
||||
files: ^\.?packit.ya?ml$
|
|
@ -0,0 +1,20 @@
|
|||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
mkdocs:
|
||||
configuration: docs/mkdocs.yml
|
||||
|
||||
# Optionally declare the Python requirements required to build your docs
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
79
Makefile
79
Makefile
|
@ -6,8 +6,20 @@ DATADIR ?= $(PREFIX)/share
|
|||
LIBDIR ?= $(PREFIX)/lib
|
||||
SYSCONFDIR?=/etc
|
||||
QMDIR=/usr/lib/qm
|
||||
SPECFILE=rpm/qm.spec
|
||||
RPM_TOPDIR ?= $(PWD)/rpmbuild
|
||||
VERSION ?= $(shell cat VERSION)
|
||||
ROOTDIR ?= $(PWD)
|
||||
|
||||
file_contexts: qm.fc
|
||||
# Default help target
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@grep -E '^##@ .*$$' $(MAKEFILE_LIST) | sed 's/##@/\n\033[1;32m/' | sed 's/$$/\033[0m/'
|
||||
|
||||
.PHONY: file_contexts
|
||||
file_contexts: qm.fc ## - Generates the qm_file_contexts
|
||||
sed \
|
||||
-e "s|${QMDIR}/rootfs||" \
|
||||
-e "s/gen_context(//g" \
|
||||
|
@ -16,29 +28,71 @@ file_contexts: qm.fc
|
|||
|
||||
all: selinux file_contexts man
|
||||
|
||||
selinux: qm.pp
|
||||
.PHONY: selinux
|
||||
selinux: qm.pp ## - Compresses the QM SELinux policy file (qm.pp)
|
||||
@echo Compressing $^ -\> $@
|
||||
bzip2 -f -9 $^
|
||||
|
||||
%.pp: %.te
|
||||
make -f ${DATADIR}/selinux/devel/Makefile $@
|
||||
mkdir -p tmp; cp qm.* tmp/
|
||||
@if ./build-aux/validations ; then \
|
||||
sed -i /user_namespace/d tmp/qm.if; \
|
||||
fi
|
||||
$(MAKE) -C tmp -f ${DATADIR}/selinux/devel/Makefile $@
|
||||
cp tmp/qm.pp .; rm -rf tmp
|
||||
|
||||
.PHONY: codespell
|
||||
codespell:
|
||||
codespell: ## - Runs codespell to check for spelling errors
|
||||
@codespell -S tmp,.git -L te -w
|
||||
|
||||
clean:
|
||||
clean: ## - Removes generated files and dirs
|
||||
rm -f *~ *.tc *.pp *.pp.bz2
|
||||
rm -rf tmp *.tar.gz
|
||||
rm -rf tmp *.tar.gz ${RPM_TOPDIR}
|
||||
|
||||
man: qm.8.md
|
||||
man: qm.8.md ## - Generates the QM man page
|
||||
go-md2man --in qm.8.md --out qm.8
|
||||
|
||||
install-policy: all
|
||||
.PHONY: dist
|
||||
dist: ## - Creates the QM distribution package
|
||||
tar cvz \
|
||||
--exclude='.git' \
|
||||
--dereference \
|
||||
--exclude='.gitignore' \
|
||||
--exclude='.fmf' \
|
||||
--exclude='.packit.*' \
|
||||
--exclude='.pre-commit*' \
|
||||
--exclude='.readthedocs.yaml' \
|
||||
--exclude='demos' \
|
||||
--exclude='docs' \
|
||||
--exclude='plans' \
|
||||
--exclude='subsystems' \
|
||||
--exclude='tests' \
|
||||
--exclude='.github' \
|
||||
--transform s/qm/qm-${VERSION}/ \
|
||||
-f /tmp/v${VERSION}.tar.gz ../qm
|
||||
mv /tmp/v${VERSION}.tar.gz ./rpm
|
||||
|
||||
.PHONY: rpm
|
||||
rpm: clean dist ## - Creates a local RPM package, useful for development
|
||||
mkdir -p ${RPM_TOPDIR}/{RPMS,SRPMS,BUILD,SOURCES}
|
||||
tools/version-update -v ${VERSION}
|
||||
cp ./rpm/v${VERSION}.tar.gz ${RPM_TOPDIR}/SOURCES
|
||||
rpmbuild -ba \
|
||||
--define="_topdir ${RPM_TOPDIR}" \
|
||||
--define="version ${VERSION}" \
|
||||
${SPECFILE}
|
||||
|
||||
.PHONY: subpackages
|
||||
subpackages: $(TARGETS)
|
||||
$(TARGETS):
|
||||
@echo "Entering directory: subsystem/$@"
|
||||
make -f subsystems/$@/Makefile $@
|
||||
|
||||
install-policy: all ## - Install selinux policies only
|
||||
semodule -i ${TARGETS}.pp.bz2
|
||||
sepolicy manpage --path . --domain ${TARGETS}_t
|
||||
|
||||
install: man all
|
||||
install: man all ## - Install QM files (including selinux)
|
||||
install -D -pm 644 ${TARGETS}.pp.bz2 ${DESTDIR}${DATADIR}/selinux/packages/qm.pp.bz2
|
||||
install -D -pm 644 qm.if ${DESTDIR}${DATADIR}/selinux/devel/include/services/qm.if
|
||||
install -D -pm 644 qm_selinux.8 ${DESTDIR}${DATADIR}/man/man8/qm_selinux.8
|
||||
|
@ -46,6 +100,13 @@ install: man all
|
|||
install -d -m 755 ${DESTDIR}${DATADIR}/qm
|
||||
install -D -m 644 qm_contexts ${DESTDIR}${DATADIR}/qm/contexts
|
||||
install -D -m 755 setup ${DESTDIR}${DATADIR}/qm/setup
|
||||
install -D -m 755 tools/comment-tz-local ${DESTDIR}${DATADIR}/qm/comment-tz-local
|
||||
install -D -m 755 tools/qm-rootfs ${DESTDIR}${DATADIR}/qm/qm-rootfs
|
||||
install -D -m 755 tools/qm-storage-settings ${DESTDIR}${DATADIR}/qm/qm-storage-settings
|
||||
install -D -m 755 create-seccomp-rules ${DESTDIR}${DATADIR}/qm/create-seccomp-rules
|
||||
install -D -m 644 qm_file_contexts ${DESTDIR}${DATADIR}/qm/file_contexts
|
||||
install -D -m 644 containers.conf ${DESTDIR}${DATADIR}/qm/containers.conf
|
||||
install -D -m 644 qm.container ${DESTDIR}${DATADIR}/containers/systemd/qm.container
|
||||
install -D -m 755 tools/qm-is-ostree ${DESTDIR}${DATADIR}/qm/qm-is-ostree
|
||||
install -D -m 755 tools/qmctl/qmctl ${DESTDIR}${PREFIX}/bin/qmctl
|
||||
install -D -m 644 tools/qmctl/qmctl.1 ${DESTDIR}${DATADIR}/man/man1/qmctl.1
|
||||
|
|
314
README.md
314
README.md
|
@ -1,33 +1,303 @@
|
|||
# QM is a containerized environment for running Functional Safety qm (Quality Management) software.
|
||||
# Topics
|
||||
|
||||
The main purpose of this package is allow users to setup an environment which
|
||||
prevents applications and container tools from interfering with other all
|
||||
other processes on the system.
|
||||
- [Topics](#topics)
|
||||
- [QM is a containerized environment for running functional safety Quality Management software](#qm-is-a-containerized-environment-for-running-functional-safety-quality-management-software)
|
||||
- [QM SELinux policy](#qm-selinux-policy)
|
||||
- [BlueChi](#bluechi)
|
||||
- [RPM building dependencies](#rpm-building-dependencies)
|
||||
- [How OOM score adjustment is used in QM](#how-oom-score-adjustment-is-used-in-qm)
|
||||
- [Priority process of OOM killer in the QM context](#priority-process-of-oom-killer-in-the-qm-context)
|
||||
- [Contributing to the QM project](#contributing-to-the-qm-project)
|
||||
- [Realtime](#realtime)
|
||||
- [Talks and videos](#talks-and-videos)
|
||||
- [RPM mirrors](#rpm-mirrors)
|
||||
- [Configuring QM](#configuring-qm)
|
||||
- [Modifying the `MemoryHigh` variable](#modifying-the-memoryhigh-variable)
|
||||
|
||||
The QM environment uses containerization tools like cgoups, namespaces, and
|
||||
security isolation to prevent accidental interference by processes in the qm.
|
||||
## QM is a containerized environment for running functional safety Quality Management software
|
||||
|
||||
The QM will run its own version of systemd and Podman to isolate not only the
|
||||
applications and containers launched by systemd and Podman but systemd and
|
||||
The main purpose of the Quality Management (QM) environment is to allow users to configure
|
||||
an environment that prevents applications and container tools from interfering with
|
||||
other processes on the system, such as in Automotive Safety Integrity Level (ASIL)
|
||||
processes and applications. AutoSD is not a certified safety product. In the context of
|
||||
AutoSD, QM is not for use in production environments but for research and learning purposes only.
|
||||
|
||||
The QM environment uses containerization tools, such as cgroups, namespaces, and
|
||||
security isolation, to prevent accidental interference by processes in the QM.
|
||||
|
||||
The QM runs its own version of systemd and Podman to isolate not only the
|
||||
applications and containers launched by systemd and Podman, but also systemd and
|
||||
Podman commands themselves.
|
||||
|
||||
This package requires the Podman package to establish the containerized
|
||||
environment and uses quadlet to set it up.
|
||||
environment and uses Quadlet to set it up. Refer to the [docs directory](docs/quadlet-examples/)
|
||||
for example Quadlet files.
|
||||
|
||||
Software install into the qm environment under /usr/lib/qm/rootfs will
|
||||
be automatically isolated from the host. But if developers want to further
|
||||
isolate these processes from other processes in the QM they can use container
|
||||
tools like Podman to further isolate.
|
||||
Software installed in the QM environment under `/usr/lib/qm/rootfs` is
|
||||
automatically isolated from the host. To further isolate these processes
|
||||
from other processes in the QM, developers can use container tools, such as Podman.
|
||||
|
||||
* SELinux Policy
|
||||
## QM SELinux policy
|
||||
|
||||
This policy is used to isolate Quality Management parts of the operating system
|
||||
from the other Domain-Specific Functional Safety Levels (ASIL).
|
||||
The QM SELinux policy isolates QM parts of the operating system
|
||||
from the other domain-specific functional safety levels, such as ASIL.
|
||||
|
||||
The main purpose of this policy is to prevent applications and container tools
|
||||
with interfering with other processes on the system. The QM needs to support
|
||||
further isolate containers run within the qm from the qm_t process and from
|
||||
each other.
|
||||
The main purpose of this policy is to prevent applications and container
|
||||
tools from interfering with other processes on the system. The QM must
|
||||
isolate containers from `qm_t` processes as well as from other containers.
|
||||
|
||||
For now all of the control processes in the qm other then containers will run
|
||||
with the same qm_t type.
|
||||
For now, all of the control processes in the QM other than containers run
|
||||
with the same `qm_t` type. For more information, refer to `man qm_selinux`.
|
||||
|
||||
For support with a specific SELinux issue, open a [QM issue](https://github.com/containers/qm/issues)
|
||||
and include the SELinux error output from a recent QM-related operation.
|
||||
|
||||
The following commands yield output that can help determine the root cause of the issue:
|
||||
|
||||
```console
|
||||
ausearch -m avc -ts recent | audit2why
|
||||
journalctl -t setroubleshoot
|
||||
sealert -a /var/log/audit/audit.log
|
||||
```
|
||||
|
||||
## BlueChi
|
||||
|
||||
- [BlueChi](https://github.com/containers/qm/pull/57)
|
||||
|
||||
The package configures the bluechi-agent within the QM.
|
||||
|
||||
BlueChi is a systemd service controller intended for use in highly regulated
|
||||
ecosystems that feature multi-node environments with a predefined number of nodes.
|
||||
Potential use cases can be found in industries that require functional safety,
|
||||
such as the transportation industry in which services must be controlled across different
|
||||
edge devices and where traditional orchestration tools do not comply with
|
||||
regulatory requirements.
|
||||
|
||||
Systems with QM installed have two systemd processes running on them. The QM
|
||||
bluechi-agent is based on the hosts `/etc/bluechi/agent.conf` file. By default, any
|
||||
changes to the system's `agent.conf` file are reflected in the QM `/etc/bluechi/agent.conf` file.
|
||||
You can further customize the QM bluechi-agent by adding content to the
|
||||
`/usr/lib/qm/rootfs/etc/bluechi/agent.conf.d/` directory.
|
||||
|
||||
```console
|
||||
# dnf install -y python3-dnf-plugins-core
|
||||
# dnf config-manager --set-enabled crb
|
||||
```
|
||||
|
||||
## RPM building dependencies
|
||||
|
||||
To build QM packages on CentOS Stream 9, enable the Code Ready Builder
|
||||
repository for access to the `golang-github-cpuguy83-md2man` package.
|
||||
|
||||
## How OOM score adjustment is used in QM
|
||||
|
||||
The Linux host kernel controls ASIL and QM processes. The Out-of-Memory (OOM) Killer is part of the Linux
|
||||
kernel's memory management subsystem. OOM Killer terminates processes to release RAM in memory-constrained conditions.
|
||||
The `oom_score_adj` parameter refers to the Out-of-Memory score adjustment in Linux operating systems.
|
||||
The OOM Killer uses the `oom_score_adj` parameter to decide which processes to terminate when the system is
|
||||
critically low on memory.
|
||||
|
||||
By fine-tuning which processes are more likely to be terminated during low-memory situations,
|
||||
critical processes can be protected, which enhances the overall stability of the system.
|
||||
|
||||
- For example, ASIL applications are essential to maintaining functional safety in automotive systems.
|
||||
You can set their OOM score adjustment value from *-1* to *-1000*. To prioritize their operation
|
||||
even in low-memory situations, setting the value to *-1000* makes the process immune to the OOM killer
|
||||
and ensures that ASIL applications are the last to be terminated.
|
||||
|
||||
- The QM process has a default OOM score adjustment value set to *500*, configured via the `qm.container` file.
|
||||
|
||||
```console
|
||||
cat /usr/share/containers/systemd/qm.container | grep OOMScoreAdjust
|
||||
# OOMScoreAdjust=500
|
||||
```
|
||||
|
||||
- All nested containers created inside the QM have a default OOM score adjustment of *750*.
|
||||
|
||||
```console
|
||||
$ cat /usr/share/qm/containers.conf | grep oom_score_adj
|
||||
oom_score_adj = 750
|
||||
```
|
||||
|
||||
### Priority process of OOM killer in the QM context
|
||||
|
||||
```txt
|
||||
+-------------------------------------------------------------+
|
||||
| The Priority Process of OOM Killer in the QM Context |
|
||||
+-------------------------------------------------------------+
|
||||
|
||||
------------------------------------ Kernel space -----------------------------------------------
|
||||
|
||||
+--------------------------------+
|
||||
| Out of Memory Killer Mechanism |
|
||||
| (OOM Killer) |
|
||||
+--------------------------------+
|
||||
|
|
||||
v
|
||||
+--------------------------------+
|
||||
| Kernel Scheduler |
|
||||
+--------------------------------+
|
||||
|
||||
------------------------------------ User space -------------------------------------------------
|
||||
|
||||
+----------------------------------------+
|
||||
| Out of Memory Score Adjustment |
|
||||
| (oom_score_adj) |
|
||||
+----------------------------------------+
|
||||
|
|
||||
|
|
||||
v (Processes Priority side by side)
|
||||
+-----------------------------+--------------------------+-----------------------+
|
||||
| | | |
|
||||
v v v v
|
||||
+------------------+ +----------------------------+ +-----------------+ +-----------------+
|
||||
| | | | | | | |
|
||||
| QM Container | | Nested Containers by QM | | ASIL Apps | | Other Processes |
|
||||
| | | | | | | |
|
||||
| OOM Score | | OOM Score | | OOM Score | | OOM Score |
|
||||
| 500 | | 750 | | -1 to -1000 | | (default: 0) |
|
||||
+------------------+ +----------------------------+ +-----------------+ +-----------------+
|
||||
| | | |
|
||||
v v v v
|
||||
+----------------+ +----------------+ +--------------------+ +-----------------+
|
||||
| Lower priority | | Higher priority| | Very low priority | | Default priority|
|
||||
| for termination| | for termination| | for termination | | for termination |
|
||||
+----------------+ +----------------+ +--------------------+ +-----------------+
|
||||
|
|
||||
|
|
||||
|
|
||||
v
|
||||
+-------------------------------------------------------------+
|
||||
| |
|
||||
| In conclusion, all nested containers created inside QM have |
|
||||
| their OOM score adjustment set to 750, making them more |
|
||||
| likely to be terminated first compared to the QM process. |
|
||||
| |
|
||||
| When compared to ASIL applications, nested containers |
|
||||
| will have an even higher likelihood of being terminated. |
|
||||
| |
|
||||
| Compared to other processes with the default adjustment |
|
||||
| value of 0, nested containers are still more likely to be |
|
||||
| terminated first, ensuring the system and ASIL apps are |
|
||||
| kept as safe as possible. |
|
||||
| |
|
||||
+-------------------------------------------------------------+
|
||||
|
||||
------------------------------------ User space -------------------------------------------------
|
||||
|
||||
------------------------------------ Kernel space -----------------------------------------------
|
||||
```
|
||||
|
||||
## Contributing to the QM project
|
||||
|
||||
For information about how to contribute to the QM project, see the [Developers documentation README](docs/devel/README.md).
|
||||
|
||||
## Realtime
|
||||
|
||||
To enable real-time removal of sched_* blockage via seccomp, use the following schema:
|
||||
|
||||
```bash
|
||||
cat << EOF >> /etc/containers/systemd/qm.container.d/rt.conf
|
||||
> [Container]
|
||||
SeccompProfile=""
|
||||
> EOF
|
||||
```
|
||||
|
||||
## Talks and videos
|
||||
|
||||
Let's spread the knowledge regarding QM. If you have interesting content pertaining to
|
||||
QM-related technology, please share it with us.
|
||||
|
||||
## RPM mirrors
|
||||
|
||||
Looking for a specific version of QM? Search the [CentOS Automotive SIG Stream Mirror](https://mirror.stream.centos.org/SIGs/9-stream/automotive/aarch64/packages-main/Packages/q/). The packages in CentOS Automotive SIG Stream Mirror are for experimentation only.
|
||||
|
||||
## Configuring QM
|
||||
|
||||
To run QM on an immutable OSTree-based OS, we use systemd units with Podman Quadlet.
|
||||
For more information on how `podman-systemd.unit` works, refer to the manual:
|
||||
|
||||
`man podman-systemd.unit`
|
||||
|
||||
The default QM configuration drop-in file is located in `/usr/share/containers/systemd/qm.container`.
|
||||
Modifying the original service file is not an option. Instead, create drop-in files to
|
||||
modify the default configuration.
|
||||
|
||||
**NOTE:** The configuration is built in alphabetical order of the drop-in files.
|
||||
|
||||
### Modifying the `MemoryHigh` variable
|
||||
|
||||
To override the default settings, create a new drop-in `.conf` file in the
|
||||
`/etc/containers/systemd/qm.container.d/` directory. This method ensures that QM memory
|
||||
usage is controlled without modifying the base system configuration.
|
||||
|
||||
1. Check the current memory limit:
|
||||
|
||||
```bash
|
||||
|
||||
systemctl show -P MemoryHigh qm
|
||||
|
||||
infinity
|
||||
|
||||
```
|
||||
|
||||
The command output `infinity` indicates that `MemoryHigh` is unlimited. You can
|
||||
see this setting in `/usr/share/containers/systemd/qm.container`.
|
||||
|
||||
1. Create a directory for the new drop-in file:
|
||||
|
||||
```bash
|
||||
|
||||
mkdir -p /etc/containers/systemd/qm.container.d/
|
||||
|
||||
```
|
||||
|
||||
1. Create a new drop-in file:
|
||||
|
||||
```bash
|
||||
vim /etc/containers/systemd/qm.container.d/100-MemoryMax.conf
|
||||
```
|
||||
|
||||
In this example, the new drop-in file is named `100-MemoryMax.conf`. You can choose a different name,
|
||||
but be aware that the configuration is built in alphabetical order of the drop-in files.
|
||||
|
||||
1. Edit the file to add the following content:
|
||||
|
||||
```bash
|
||||
|
||||
[Service]
|
||||
|
||||
MemoryHigh=2G
|
||||
|
||||
```
|
||||
|
||||
`MemoryHigh` is specified in gigabytes. 2G means 2 gigabytes.
|
||||
|
||||
1. Preview the updated systemd configuration:
|
||||
|
||||
```bash
|
||||
|
||||
/usr/lib/systemd/system-generators/podman-system-generator {--user} --dryrun
|
||||
|
||||
```
|
||||
|
||||
1. Reload systemd and restart `qm.service` to apply the configuration changes:
|
||||
|
||||
```bash
|
||||
|
||||
systemctl daemon-reload
|
||||
|
||||
systemctl restart qm.service
|
||||
|
||||
```
|
||||
|
||||
1. Verify the value of `MemoryHigh`:
|
||||
|
||||
```bash
|
||||
|
||||
systemctl show -P MemoryHigh qm
|
||||
|
||||
2147483648
|
||||
```
|
||||
|
||||
Memory values are displayed in bytes; 2147483648 bytes = 2G, which confirms that `MemoryHigh` is set to 2G.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
## Security and Disclosure Information Policy for the qm Project
|
||||
# Security and Disclosure Information Policy for the qm Project
|
||||
|
||||
The qm Project follows the [Security and Disclosure Information Policy](https://github.com/containers/common/blob/master/SECURITY.md) for the Containers Projects.
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.git
|
||||
.gitignore
|
||||
demos
|
||||
tests
|
||||
rpmbuild
|
||||
tools
|
||||
subsystems
|
||||
qm-windowmanager
|
||||
.github
|
||||
NOTICE
|
||||
plans
|
||||
create-seccomp-rules
|
||||
qm_file_context
|
||||
qm.te
|
||||
build-aux
|
||||
docs
|
||||
qm_contexts
|
||||
qm.if
|
||||
qm_selinux.8
|
||||
setup
|
||||
rpm
|
||||
qm.8.md
|
||||
qm.fc
|
||||
pre-commit-hooks
|
||||
pre-commit-hooks/validate-config.sh
|
||||
Makefile
|
||||
.fmf/
|
||||
.fmf/version
|
||||
.packit.sh
|
||||
.packit.yaml
|
||||
.pre-commit-config.yaml
|
||||
.pre-commit-hooks.yaml
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/bash
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source /etc/os-release
|
||||
|
||||
versionid=$(echo "$VERSION_ID" | awk -F. '{print $1}')
|
||||
id_os=$(echo "$ID" | grep -i -E "(centos|rhel|autosd)")
|
||||
|
||||
if [ "${versionid}" -le 9 ] && [ -n "${id_os}" ] ; then
|
||||
# the sed command is required for the selinux
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit 1
|
|
@ -1,8 +1,29 @@
|
|||
[containers]
|
||||
default_ulimits = []
|
||||
default_sysctls = []
|
||||
|
||||
cgroup_conf=[
|
||||
"memory.oom.group=1",
|
||||
]
|
||||
|
||||
# Temporary default to host network until we fix private network bridge setup
|
||||
# when the qm container doesn't unmask all the virtual filesystems.
|
||||
netns="host"
|
||||
|
||||
# The om_score_adj refers to the "Out of Memory score adjustment" in Linux
|
||||
# operating systems. This parameter is used by the Out of Memory (OOM)
|
||||
# killer to decide which processes to terminate when the system is
|
||||
# critically low on memory.
|
||||
#
|
||||
# All nested containers created with QM will be set to 750 as score.
|
||||
# The QM process has default value as 500 set via qm.container file.
|
||||
#
|
||||
# $ cat qm.container | grep OOMScoreAdjust
|
||||
# OOMScoreAdjust=500
|
||||
#
|
||||
oom_score_adj = 750
|
||||
|
||||
[network]
|
||||
# The default is 10.88.0.0, but we need qm containers to have a
|
||||
# different ip address range or routing becomes confused
|
||||
default_subnet="10.89.0.0/16"
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
SECCOMP_CONTAINERS_FILE="/usr/share/containers/seccomp.json"
|
||||
SYSCALLS_TO_DENY=("sched_setscheduler" "sched_setattr")
|
||||
|
||||
QM_PATH_SECCOMP="/usr/share/qm/seccomp-no-rt.json"
|
||||
QM_DIR="${QM_PATH_SECCOMP%/*}"
|
||||
|
||||
function remove_seccomp_entry_from_allow() {
|
||||
local syscall_name=$1
|
||||
local seccomp_file_path=$2
|
||||
|
||||
temp_file=$(mktemp)
|
||||
jq --tab \
|
||||
--arg syscall "$syscall_name" \
|
||||
'(.syscalls[] | select(.names[] == $syscall and .action == "SCMP_ACT_ALLOW").names) |= map(select(. != $syscall))' \
|
||||
"${seccomp_file_path}" > "$temp_file" && mv "$temp_file" "${seccomp_file_path}"
|
||||
|
||||
rm "$temp_file" &> /dev/null
|
||||
}
|
||||
|
||||
function add_syscall_deny_list() {
|
||||
local syscall_name="$1"
|
||||
local seccomp_file_path="$2"
|
||||
|
||||
local temp_file
|
||||
temp_file=$(mktemp)
|
||||
|
||||
if [[ "$syscall_name" == "sched_setscheduler" ]]; then
|
||||
jq --tab \
|
||||
'.syscalls += [
|
||||
{
|
||||
"names": ["sched_setscheduler"],
|
||||
"action": "SCMP_ACT_ALLOW",
|
||||
"args": [
|
||||
{
|
||||
"index": 1,
|
||||
"value": 0,
|
||||
"valueTwo": 0,
|
||||
"op": "SCMP_CMP_EQ"
|
||||
}
|
||||
],
|
||||
"comment": "",
|
||||
"includes": {},
|
||||
"excludes": {}
|
||||
},
|
||||
{
|
||||
"names": ["sched_setscheduler"],
|
||||
"action": "SCMP_ACT_ALLOW",
|
||||
"args": [
|
||||
{
|
||||
"index": 1,
|
||||
"value": 3,
|
||||
"valueTwo": 0,
|
||||
"op": "SCMP_CMP_EQ"
|
||||
}
|
||||
],
|
||||
"comment": "",
|
||||
"includes": {},
|
||||
"excludes": {}
|
||||
},
|
||||
{
|
||||
"names": ["sched_setscheduler"],
|
||||
"action": "SCMP_ACT_ALLOW",
|
||||
"args": [
|
||||
{
|
||||
"index": 1,
|
||||
"value": 5,
|
||||
"valueTwo": 0,
|
||||
"op": "SCMP_CMP_EQ"
|
||||
}
|
||||
],
|
||||
"comment": "",
|
||||
"includes": {},
|
||||
"excludes": {}
|
||||
}
|
||||
]' "$seccomp_file_path" > "$temp_file" && mv "$temp_file" "$seccomp_file_path"
|
||||
else
|
||||
jq --tab \
|
||||
--arg syscall "$syscall_name" \
|
||||
'.syscalls += [{
|
||||
"names": [$syscall],
|
||||
"action": "SCMP_ACT_ERRNO",
|
||||
"args": [],
|
||||
"errnoRet": 1,
|
||||
"errno": "EPERM"
|
||||
}]' "$seccomp_file_path" > "$temp_file" && mv "$temp_file" "$seccomp_file_path"
|
||||
fi
|
||||
|
||||
rm "$temp_file" &> /dev/null
|
||||
}
|
||||
|
||||
# Main
|
||||
if [ ! -f "${SECCOMP_CONTAINERS_FILE}" ]; then
|
||||
echo "Exiting... unable to find ${SECCOMP_CONTAINERS_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${QM_DIR}" ]; then
|
||||
echo "Exiting... unable to find ${QM_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Copying original seccomp.json
|
||||
cp "${SECCOMP_CONTAINERS_FILE}" "${QM_PATH_SECCOMP}"
|
||||
|
||||
for syscall in "${SYSCALLS_TO_DENY[@]}"; do
|
||||
# Remove syscall entry from the allow list
|
||||
remove_seccomp_entry_from_allow "${syscall}" "${QM_PATH_SECCOMP}"
|
||||
|
||||
# Add syscall to the deny list
|
||||
add_syscall_deny_list "${syscall}" "${QM_PATH_SECCOMP}"
|
||||
done
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/python
|
||||
|
||||
"""
|
||||
Enable se_linux permission to socket.
|
||||
|
||||
Script enable se_linux fscreate and socketcreate
|
||||
"""
|
||||
|
||||
import selinux
|
||||
import socket
|
||||
import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
|
||||
socket_dir = "/run/asilservice"
|
||||
socket_path = socket_dir + "/asilservice.sock"
|
||||
if os.path.exists(socket_dir):
|
||||
shutil.rmtree(socket_dir)
|
||||
|
||||
try:
|
||||
selinux.setfscreatecon("system_u:object_r:qm_file_t:s0")
|
||||
selinux.setsockcreatecon("system_u:system_r:qm_t:s0")
|
||||
except BaseException as see:
|
||||
# selinux is disabled
|
||||
print("\nSELinux is disabled, such environment is not recommended\n")
|
||||
print("\nSELinux Exception occured:", see)
|
||||
pass
|
||||
|
||||
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
os.mkdir(socket_dir)
|
||||
server.bind(socket_path)
|
||||
server.listen(1)
|
||||
conn, addr = server.accept()
|
||||
try:
|
||||
while True:
|
||||
data = conn.recv(1024)
|
||||
conn.send("ASIL reply: ".encode())
|
||||
conn.send(data)
|
||||
if data.decode().strip() == "goodbye" or \
|
||||
data.decode().strip() == "quit":
|
||||
break
|
||||
except Exception as error:
|
||||
print("Exception occured:", error)
|
||||
|
||||
print(sys.argv[0] + ": exited\n")
|
||||
conn.close()
|
|
@ -0,0 +1 @@
|
|||
../tests/e2e/dashboard-tui/
|
|
@ -0,0 +1,227 @@
|
|||
#!/usr/bin/env bash
|
||||
# shellcheck disable=SC2120,SC1091,SC2181
|
||||
|
||||
# Setting up some colors for helping read the demo output.
|
||||
# Comment out any of the below to turn off that color.
|
||||
bold=$(tput bold)
|
||||
cyan=$(tput setaf 6)
|
||||
reset=$(tput sgr0)
|
||||
export qm_quadlet=/etc/containers/systemd/qm.container
|
||||
export rootfs=/usr/lib/qm/rootfs
|
||||
|
||||
read_color() {
|
||||
echo -e "\n\n[Press enter to continue]"
|
||||
read -r -p "${bold}$1${reset}"
|
||||
}
|
||||
|
||||
exec_color() {
|
||||
echo -e "\nCommand to be executed:"
|
||||
echo -e "
|
||||
\t${bold}$ $1${reset}\n"
|
||||
read -rp "[Press enter to continue]"
|
||||
eval "${1}"
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "command has failed ${1}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
echo_color() {
|
||||
echo "${cyan}$1${reset}"
|
||||
}
|
||||
|
||||
check_command() {
|
||||
required_cmds=(sudo podman dnf)
|
||||
for required_cmd in "${required_cmds[@]}"; do
|
||||
if ! command -v "${required_cmd}" &> /dev/null
|
||||
then
|
||||
echo "${1} command could not be found. Install ${1} to continue."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
init() {
|
||||
clear
|
||||
check_command
|
||||
|
||||
if [ ! -f "./asilservice" ]; then
|
||||
echo "The asilservice script not found, please check."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo_color "Starting demo..."
|
||||
echo -e " - Removing any previous configuration for qm...\n"
|
||||
rpm -e qm &> /dev/null
|
||||
sudo systemctl stop qm &> /dev/null
|
||||
sudo podman rm qm --force -t 0 > /dev/null
|
||||
sudo podman volume rm --force qmEtc qmVar > /dev/null
|
||||
sudo podman --root /var/lib/shared rmi -i ubi9 > /dev/null
|
||||
sudo rm -rf /usr/lib/qm > /dev/null
|
||||
test -s ${qm_quadlet}.orig && sudo mv ${qm_quadlet}.orig ${qm_quadlet}
|
||||
}
|
||||
|
||||
install() {
|
||||
clear
|
||||
bluechi_snapshot='dnf copr enable @centos-automotive-sig/bluechi-snapshot -y &> /dev/null'
|
||||
eval "${bluechi_snapshot}"
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "fail to enable copr."
|
||||
fi
|
||||
|
||||
cmd_install='sudo dnf -y install qm \
|
||||
bluechi bluechi-agent bluechi-ctl \
|
||||
podman container-selinux; \
|
||||
sudo dnf -y update qm bluechi bluechi-agent \
|
||||
bluechi-ctl podman container-selinux'
|
||||
|
||||
echo_color "Installing packages"
|
||||
eval "${cmd_install}"
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "fail to install packages."
|
||||
fi
|
||||
clear
|
||||
}
|
||||
|
||||
setup() {
|
||||
echo_color "Executing setup"
|
||||
echo_color "Staring and enabling bluechi on the host system"
|
||||
echo_color "============================================="
|
||||
exec_color "sudo systemctl start bluechi bluechi-agent"
|
||||
|
||||
echo
|
||||
|
||||
echo_color "Install and setup: /usr/lib/qm/rootfs"
|
||||
echo_color "============================================="
|
||||
exec_color "sudo /usr/share/qm/setup"
|
||||
read -rp "[Press enter to continue]"
|
||||
clear
|
||||
}
|
||||
|
||||
status() {
|
||||
exec_color "sudo systemctl status qm.service"
|
||||
clear
|
||||
}
|
||||
|
||||
cpuweight() {
|
||||
exec_color "sudo systemctl set-property --runtime qm.service CPUWeight=50"
|
||||
|
||||
echo "Value stored in qm.service/cpu.weight:"
|
||||
sudo cat /sys/fs/cgroup/qm.service/cpu.weight
|
||||
|
||||
exec_color "sudo systemctl set-property --runtime qm.service CPUWeight=10"
|
||||
|
||||
echo "Value stored in qm.service/cpu.weight:"
|
||||
sudo cat /sys/fs/cgroup/qm.service/cpu.weight
|
||||
echo -e "\n\n[Press enter to continue]"
|
||||
read -r
|
||||
}
|
||||
|
||||
podman_ctr() {
|
||||
clear
|
||||
exec_color "sudo podman exec -ti qm ps -eZ"
|
||||
exec_color "sudo podman exec qm podman run alpine echo hi"
|
||||
exec_color "sudo podman run ubi9 echo hi"
|
||||
exec_color "sudo podman exec qm podman images"
|
||||
exec_color "sudo podman images"
|
||||
exec_color "sudo podman exec qm podman run --userns=auto alpine cat /proc/self/uid_map"
|
||||
exec_color "sudo podman exec qm podman run --userns=auto alpine cat /proc/self/uid_map"
|
||||
exec_color "sudo podman exec qm podman run --userns=auto alpine cat /proc/self/uid_map"
|
||||
exec_color "sudo podman run --userns=auto ubi9 cat /proc/self/uid_map"
|
||||
exec_color "sudo podman run --userns=auto ubi9 cat /proc/self/uid_map"
|
||||
}
|
||||
|
||||
bluechi() {
|
||||
clear
|
||||
exec_color "sudo podman exec -ti qm podman pull registry.access.redhat.com/ubi8/httpd-24:latest"
|
||||
exec_color "echo \"[Container]
|
||||
Image=registry.access.redhat.com/ubi8/httpd-24
|
||||
Network=host
|
||||
\" > /tmp/myquadlet.container"
|
||||
|
||||
exec_color "sudo podman cp /tmp/myquadlet.container qm:/etc/containers/systemd/"
|
||||
exec_color "sudo podman exec qm systemctl daemon-reload"
|
||||
|
||||
# read the actual configuration.. and add the new qm.control
|
||||
source /etc/bluechi/bluechi.conf
|
||||
|
||||
# Append to the current configuration the new node and restart the service
|
||||
sudo sed -i '/^AllowedNodeNames=/ s/$/,qm.control/' /etc/bluechi/bluechi.conf
|
||||
exec_color "sudo systemctl restart bluechi"
|
||||
|
||||
# get the ip of controller via interface eth0
|
||||
ip_controller=$(ip addr list eth0 |grep "inet " |cut -d' ' -f6|cut -d/ -f1)
|
||||
eval "$(podman exec qm \
|
||||
sed -i 's/^#ControllerHost=/ControllerHost='"${ip_controller}"'/g' \
|
||||
/etc/bluechi/agent.conf
|
||||
)"
|
||||
eval "$(podman exec qm \
|
||||
systemctl restart bluechi-agent
|
||||
)"
|
||||
|
||||
exec_color "sudo bluechictl restart qm.control myquadlet.service"
|
||||
exec_color "sudo bluechictl list-units | grep --color myquadlet"
|
||||
|
||||
# give time to server spin up
|
||||
sleep 3
|
||||
|
||||
exec_color "curl 127.0.0.1:8080"
|
||||
exec_color "sudo bluechictl stop qm.control myquadlet.service"
|
||||
|
||||
# requires true as return -1 (to show that myquadlet is gonet)
|
||||
exec_color "sudo bluechictl list-units | grep --color myquadlet || true"
|
||||
}
|
||||
|
||||
additional_store() {
|
||||
test -s ${qm_quadlet}.orig || sudo cp ${qm_quadlet} ${qm_quadlet}.orig
|
||||
exec_color "sudo podman --root /var/lib/shared pull ubi9"
|
||||
exec_color "sudo podman --root /var/lib/shared images | grep -A 10 -B 10 --color ubi9"
|
||||
|
||||
# requires true as return -1 (to show the image doesn't exist)
|
||||
exec_color "sudo podman exec qm podman images | grep -A 10 -B 10 --color ubi9 || true"
|
||||
|
||||
exec_color "sudo sed -i -e '\$aVolume=/var/lib/shared:/var/lib/shared:ro' /etc/containers/systemd/qm.container"
|
||||
exec_color "sudo systemctl daemon-reload"
|
||||
exec_color "sudo systemctl restart qm.service"
|
||||
exec_color "sudo podman exec qm podman images | grep -A 10 -B 10 --color ubi9"
|
||||
}
|
||||
|
||||
asilservice() {
|
||||
# requires true as return -1 (to show that the asilservice is not available)
|
||||
exec_color "grep -A 20 -B 20 --color system_u.*s0 asilservice || true"
|
||||
|
||||
exec_color "sudo mkdir -p /run/asilservice"
|
||||
exec_color "sudo python ./asilservice &"
|
||||
exec_color "sudo systemctl stop qm"
|
||||
sudo -u root "podman volume rm --force qmEtc qmVar > /dev/null"
|
||||
exec_color "sudo dnf -y install --installroot ${rootfs} nmap-ncat --nogpgcheck"
|
||||
exec_color "sudo restorecon -R ${rootfs}"
|
||||
exec_color "sudo sed -i -e '\$aVolume=/run/asilservice:/run/asilservice' /etc/containers/systemd/qm.container"
|
||||
exec_color "sudo systemctl daemon-reload"
|
||||
exec_color "sudo systemctl restart qm"
|
||||
|
||||
echo_color "Starting asilservice, type any command to get a reply from ASIL service or quit to exit."
|
||||
# requires true as return -1 as users type quit or goodbye
|
||||
exec_color "sudo podman exec -ti qm /usr/bin/nc -U /run/asilservice/asilservice.sock || true"
|
||||
}
|
||||
|
||||
init
|
||||
|
||||
install
|
||||
|
||||
setup
|
||||
|
||||
status
|
||||
|
||||
cpuweight
|
||||
|
||||
podman_ctr
|
||||
|
||||
bluechi
|
||||
|
||||
additional_store
|
||||
|
||||
asilservice
|
||||
|
||||
echo "Demo is done"
|
Binary file not shown.
|
@ -0,0 +1,77 @@
|
|||
# DevConf2024
|
||||
|
||||
- [Introduction](## Introduction)
|
||||
- [Requirements](## Requirements)
|
||||
- [Install](## Running the demo)
|
||||
|
||||
## Introduction
|
||||
|
||||
This demo runs uninterfered service containers, process, under the following:
|
||||
|
||||
- HOST partition
|
||||
- QM partition
|
||||
|
||||
It demonstartes:
|
||||
|
||||
- How to use quadlet service files under HOST and QM partitions
|
||||
- How to control services with bluechictl
|
||||
|
||||
Running the demo recommend environment for tests is inside CentOS-Stream-9
|
||||
|
||||
## Requirements
|
||||
|
||||
It is recommend to use vm environment for tests in CentOS-Stream-9 virtual machine
|
||||
|
||||
Follow the following procedure to prepare virtual machine
|
||||
[Download and run vm](../../tests/e2e/README.md#run-tmt-tests-framework-locally)
|
||||
|
||||
ssh to vm
|
||||
|
||||
``` bash
|
||||
ssh -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null "root@localhost" -p <port>
|
||||
```
|
||||
|
||||
## Install and set QM partition
|
||||
|
||||
Once inside VM, install rpms
|
||||
|
||||
``` bash
|
||||
dnf install git python3-pip podman
|
||||
```
|
||||
|
||||
Under root directory run the following git commands
|
||||
|
||||
``` bash
|
||||
git clone --depth=1 https://github.com/containers/qm.git
|
||||
cd qm
|
||||
git checkout -b devconf-update
|
||||
git pull origin devconf-update
|
||||
git sparse-checkout set --no-cone demos/devconf-2024
|
||||
git sparse-checkout add tests/e2e
|
||||
```
|
||||
|
||||
Run setup script, it installs & prepares QM partition filesystem
|
||||
|
||||
``` bash
|
||||
cd demos/devconf-2024
|
||||
bash -x setup
|
||||
```
|
||||
|
||||
Check QM partition is running and active
|
||||
|
||||
``` bash
|
||||
systemctl is-active qm
|
||||
active
|
||||
```
|
||||
|
||||
## Prepare demo services
|
||||
|
||||
The demo create containers under HOST partition and QM partition
|
||||
|
||||
``` bash
|
||||
bash -x prepare-demo.sh
|
||||
```
|
||||
|
||||
## Running the demo
|
||||
|
||||
Please refer the following demo (ADD LINK)
|
|
@ -0,0 +1 @@
|
|||
../../tests/e2e/lib/
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2023 The qm Authors
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; If not, see <http://www.gnu.org/licenses/>.
|
||||
# Capture the start time
|
||||
|
||||
START_TIME=$(date +%s)
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
# Add menu
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "${SCRIPT_DIR}"/lib/systemd
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "${SCRIPT_DIR}"/lib/utils
|
||||
|
||||
# Cleanup
|
||||
cleanup_node_services
|
||||
|
||||
# Create host services
|
||||
echo
|
||||
info_message "Preparing HOST services"
|
||||
info_message "=============================="
|
||||
|
||||
srvs_host=("safety" "cruise_control" "tires" "breaks")
|
||||
create_stub_systemd_srv "" "" "${srvs_host[@]}"
|
||||
|
||||
# Run verification for HOST
|
||||
for service in "${srvs_host[@]}"; do
|
||||
bluechictl list-units localrootfs --filter \
|
||||
"*${service}*" | grep active
|
||||
if_error_exit "HOST ${service} service is not in running state"
|
||||
done
|
||||
|
||||
info_message "${GRN}HOST services are running ${CLR}"
|
||||
|
||||
# Create QM services
|
||||
echo
|
||||
info_message "Preparing QM services"
|
||||
info_message "=============================="
|
||||
|
||||
srvs_qm=("radio" "store" "stream_audio" "maps")
|
||||
echo
|
||||
create_stub_systemd_srv "" "qm" "${srvs_qm[@]}"
|
||||
|
||||
# Run verification for QM
|
||||
for service in "${srvs_qm[@]}"; do
|
||||
bluechictl list-units qm.localrootfs --filter \
|
||||
"*${service}*" | grep active
|
||||
if_error_exit "QM ${service} service is not in running state"
|
||||
done
|
||||
|
||||
info_message "${GRN}QM services are running ${CLR}"
|
||||
|
||||
# Capture the end time
|
||||
END_TIME=$(date +%s)
|
||||
|
||||
# Calculate the duration in seconds
|
||||
DURATION=$((END_TIME - START_TIME))
|
||||
|
||||
# Calculate minutes and seconds
|
||||
DAYS=$((DURATION / 86400))
|
||||
HOURS=$(( (DURATION % 86400) / 3600 ))
|
||||
MINUTES=$(( (DURATION % 3600) / 60 ))
|
||||
SECONDS=$((DURATION % 60))
|
||||
|
||||
echo
|
||||
info_message "${GRN}Running time for this script${CLR}"
|
||||
info_message "\t- ${DAYS} days, ${HOURS} hours, ${MINUTES} minutes and ${SECONDS} seconds"
|
|
@ -0,0 +1 @@
|
|||
../../tests/e2e/set-ffi-env-e2e
|
|
@ -0,0 +1 @@
|
|||
../../tests/e2e/tools/
|
|
@ -0,0 +1,109 @@
|
|||
# Developers documentation
|
||||
|
||||
## Building QM rpm manually with changes
|
||||
|
||||
Building QM locally with changes for tests is a recommended practice,
|
||||
especially for testing new features before submitting a pull request.
|
||||
|
||||
**1.** Prerequisite
|
||||
|
||||
```bash
|
||||
dnf install -y rpm-build golang-github-cpuguy83-md2man selinux-policy-devel
|
||||
```
|
||||
|
||||
**2.** Clone the repo
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
```
|
||||
|
||||
**3.** Build the RPM
|
||||
|
||||
Select a QM version that is a higher number from the current one.
|
||||
For example, if today's QM version is 0.6.2, set it to 1.0 so that
|
||||
the RPM created is identifiable as yours.
|
||||
|
||||
```bash
|
||||
make clean && VERSION=1.0 make rpm
|
||||
```
|
||||
|
||||
The rpm is created at the `${RPM_TOPDIR}/RPMS` folder, by default
|
||||
`${PWD}/rpmbuild/RPMS`.
|
||||
You can export **RPM_TOPDIR** to change the path where the rpm will be placed.
|
||||
For example:
|
||||
|
||||
```bash
|
||||
VERSION=1.0 RPM_TOPDIR=/USER/rpmbuild make rpm
|
||||
```
|
||||
|
||||
## Building CentOS AutoSD and QM manually
|
||||
|
||||
During development, it is common to conduct integration tests to ensure your
|
||||
changes work well with other components within the overall solution.
|
||||
In our case, it's best to test against the CentOS Automotive Stream
|
||||
Distribution (AutoSD) image.
|
||||
|
||||
Once you have the new [RPM](#building-qm-rpm-manually-with-changes), follow these steps:
|
||||
|
||||
**1.** Make sure the new rpm is located in **/USER/rpmbuild/RPMS/**
|
||||
|
||||
Example
|
||||
|
||||
```bash
|
||||
ls /root/rpmbuild/RPMS/noarch/qm-1.0-1.noarch.rpm
|
||||
/root/rpmbuild/RPMS/noarch/qm-1.0-1.noarch.rpm
|
||||
```
|
||||
|
||||
**2.** Download additional packages required by the image
|
||||
|
||||
```bash
|
||||
sudo dnf download --destdir /root/rpmbuild/RPMS/noarch/ selinux-policy selinux-policy-any
|
||||
```
|
||||
|
||||
**3.** Create a local repository with the new package
|
||||
|
||||
```bash
|
||||
dnf install createrepo_c -y
|
||||
cd /root/rpmbuild/RPMS/noarch/
|
||||
createrepo .
|
||||
```
|
||||
|
||||
**4.** Clone the CentOS Automotive distro for the build
|
||||
|
||||
Ensure you meet the requirements for the CentOS Automotive Stream by
|
||||
referring to [this link](https://sigs.centos.org/automotive/building/).
|
||||
|
||||
The following commands will execute:
|
||||
|
||||
- Install the podman package
|
||||
- Clone the sample-images repository and required submodules (automotive-image-builder)
|
||||
- Cleanups before a fresh build
|
||||
- Finally creates a new qcow2 image (BASED ON distro name, mode (ostree or regular) and uses the qemu-qm-container sample image)
|
||||
NOTE:
|
||||
- The path for the new QM rpm file (/root/rpmbuild/RPMS/noarch)
|
||||
- extra_rpms - useful for debug.
|
||||
- ssh enabled
|
||||
|
||||
The command below utilises automotive-image-builder to produce a `qm-minimal` qcow2 image for cs9,
|
||||
other example images such as `simple-qm-container` and the `simple-qm`
|
||||
image can be found in the images directory of the sample-images repository.
|
||||
|
||||
```bash
|
||||
dnf install podman -y && dnf clean all
|
||||
git clone https://gitlab.com/CentOS/automotive/sample-images.git
|
||||
git submodule update --init
|
||||
cd sample-images/
|
||||
rm -rf _build #Optional, only relevant after initial build
|
||||
rm -rf *.qcow2 #Optional, only relevant after initial build
|
||||
./automotive-image-builder/automotive-image-builder build --distro cs9 --mode package --define 'ssh_permit_root_login=true' --define 'ssh_permit_password_auth=true' --define 'extra_repos=[{"id":"local","baseurl":"file:///root/rpmbuild/RPMS/noarch"}]' --define 'extra_rpms=["qm-1.0", "vim-enhanced", "openssh-server", "openssh-clients", "python3", "polkit", "rsync", "strace", "dnf", "gdb"]' --target qemu --export qcow2 images/qm-minimal.mpp.yml cs9-qemu-qm-container.x86_64.qcow2
|
||||
```
|
||||
|
||||
If you would like more information on building automotive images with automotive-image-builder, please see the
|
||||
[Automotive SIG pages for AutoSD](https://sigs.centos.org/automotive/getting-started/about-automotive-image-builder/)
|
||||
|
||||
Run the virtual machine, default user: root, pass: password.
|
||||
To change default values, use the [defaults.ipp.yml](https://gitlab.com/CentOS/automotive/src/automotive-image-builder/-/blob/main/include/defaults.ipp.yml) file.
|
||||
|
||||
```bash
|
||||
./automotive-image-builder/automotive-image-runner --nographics ./cs9-qemu-qm-container.x86_64.qcow2
|
||||
```
|
|
@ -0,0 +1,21 @@
|
|||
# Maintainer documentation
|
||||
|
||||
## Creating a new release
|
||||
|
||||
Initially, make sure to [bump **qm.te** and **VERSION** files in the git repo](https://github.com/containers/qm/pull/760) to the next release, i.e: *v0.7.5*.
|
||||
After that, follow the steps below using GitHub UI.
|
||||
|
||||
**Create a new Release**
|
||||

|
||||
|
||||
**Draft a new release**
|
||||

|
||||
|
||||
**Create a new tag**
|
||||

|
||||
|
||||
**Generate release notes**
|
||||

|
||||
|
||||
**Publish Release**
|
||||

|
Binary file not shown.
After Width: | Height: | Size: 513 KiB |
Binary file not shown.
After Width: | Height: | Size: 943 KiB |
Binary file not shown.
After Width: | Height: | Size: 374 KiB |
Binary file not shown.
After Width: | Height: | Size: 405 KiB |
Binary file not shown.
After Width: | Height: | Size: 500 KiB |
|
@ -0,0 +1,536 @@
|
|||
# Subpackages
|
||||
|
||||
Subpackages are **experimental approach** to deliver in a single point (RPM) dropin files
|
||||
and additional requirements.
|
||||
|
||||
The qm project is designed to provide a flexible and modular environment for managing
|
||||
Quality Management (QM) software in containerized environments. One of the key features
|
||||
of the qm package is its support for sub-package(s), such as the qm-dropin sub-packages.
|
||||
These sub-packages are not enabled by default and are optional. However, allow users
|
||||
to easily extend or customize their QM environment by adding specific configurations,
|
||||
tools, or scripts to the containerized QM ecosystem by simple installing or uninstalling
|
||||
a RPM package into the system.
|
||||
|
||||
The key features of QM Sub-Packages are
|
||||
|
||||
- **Modularity**
|
||||
- No configuration change, no typo or distribution rebuild/update.
|
||||
- Just dnf install/remove from the traditional rpm schema.
|
||||
- **Customizability**
|
||||
- Users can easily add specific configurations to enhance or modify the behavior of their QM containers.
|
||||
- **Maintainability**
|
||||
- Sub-packages ensure that the base qm package remains untouched, allowing easy updates without breaking custom configurations.
|
||||
- **Simplicity**
|
||||
- Like qm-dropin provide a clear directory structure and templates to guide users in customizing their QM environment.
|
||||
|
||||
!!! note
|
||||
The following sections describe the currently available QM subpackages.
|
||||
|
||||
## Building QM sub-packages
|
||||
|
||||
Choose one of the following sub-packages and build using make.
|
||||
|
||||
```bash
|
||||
git clone git@github.com:containers/qm.git && cd qm
|
||||
|
||||
Example of subpackages: kvm, qm-oci-hooks, ros2, text2speech, windowmanager
|
||||
|
||||
make TARGETS=input subpackages
|
||||
ls rpmbuild/RPMS/noarch/
|
||||
qm-0.6.7-1.fc40.noarch.rpm qm_mount_bind_input-0.6.7-1.fc40.noarch.rpm
|
||||
```
|
||||
|
||||
## Installing QM sub-packages
|
||||
|
||||
```bash
|
||||
$ sudo dnf install ./rpmbuild/RPMS/noarch/qm_mount_bind_input-0.6.7-1.fc40.noarch.rpm
|
||||
<SNIP>
|
||||
Complete!
|
||||
```
|
||||
|
||||
If QM is already running, restart or reload your QM container environment to apply the new configurations.
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo podman restart qm
|
||||
```
|
||||
|
||||
## Removing QM sub-packages
|
||||
|
||||
```bash
|
||||
sudo rpm -e qm_mount_bind_input
|
||||
```
|
||||
|
||||
## Creating your own drop-in QM sub-package
|
||||
|
||||
We recommend using the existing drop-in files as a guide and adapting them to your specific needs. However, here are the step-by-step instructions:
|
||||
|
||||
1) Add a drop-in file to: `etc/containers/systemd/qm.container.d/qm_dropin_<subpackage>.conf>`
|
||||
2) Add your package as a sub-package to: `rpm/<subpackage_directory>/<subpackage>.spec`
|
||||
3) Add the makefile for the sub-package and any files required by the sub-package to: `subsystems/<subpackage_directory>`
|
||||
4) Test the sub-package build by running: `make clean && make TARGETS=<subpackage> subpackages`
|
||||
5) Install your sub-package using: `dnf install -y rpmbuild/RPMS/noarch/<subpackage>*.noarch.rpm`
|
||||
6) Restart podman container using: `sudo podman restart qm`
|
||||
7) Additionally, test it with and without enabling the sub-package using (by default it should be disabled but there are cases where it will be enabled by default if QM community decide):
|
||||
|
||||
Example changing the spec and triggering the build via make (feel free to automate via sed, awk etc):
|
||||
|
||||
```bash
|
||||
# Use make file to run specific subpackage
|
||||
make TARGETS=windowmanager subpackages
|
||||
```
|
||||
|
||||
## QM sub-package Video
|
||||
|
||||
The video sub-package exposes `/dev/video0` (or many video devices required) to the container. This feature is useful for demonstrating how to share a camera from the host system into a container using Podman drop-in. To showcase this functionality, we provide the following demo:
|
||||
|
||||
### Building the video sub-package, installing, and restarting QM
|
||||
|
||||
```bash
|
||||
make TARGETS=video subpackages
|
||||
sudo dnf install ./rpmbuild/RPMS/noarch/qm-mount-bind-video-0.6.7-1.fc40.noarch.rpm
|
||||
sudo systemctl daemon-reload
|
||||
sudo podman restart qm
|
||||
```
|
||||
|
||||
This simulates a rear camera when the user shifts into reverse gear.
|
||||
|
||||
In this simulation, we created a systemd service that, every time it is started, captures a snapshot from the webcam, simulating the action of a rear camera. (Feel free to start and restart the service multiple times!)
|
||||
|
||||
```bash
|
||||
host> sudo podman exec -it qm bash
|
||||
|
||||
bash-5.2# systemctl daemon-reload
|
||||
bash-5.2# systemctl start rear-camera
|
||||
|
||||
# ls -la /var/tmp/screenshot.jpg
|
||||
-rw-r--r--. 1 root root 516687 Oct 13 04:05 /var/tmp/screenshot.jpg
|
||||
bash-5.2#
|
||||
```
|
||||
|
||||
### Copy the screenshot to the host and view it
|
||||
|
||||
```bash
|
||||
host> sudo podman cp qm:/var/tmp/screenshot.jpg .
|
||||
```
|
||||
|
||||
Great job! Now imagine all the possibilities this opens up!
|
||||
|
||||
## QM sub-package Sound
|
||||
|
||||
### Step 1: Install the QM Mount Bind Sound Package
|
||||
|
||||
To set up sound cards in a QM environment using Podman, follow the steps below:
|
||||
Run the following commands to install the `qm_mount_bind_sound` package and restart QM (if previously in use):
|
||||
|
||||
```bash
|
||||
# Build and install the RPM for QM sound
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
make TARGETS=sound subpackages
|
||||
sudo dnf install -y rpmbuild/RPMS/noarch/qm_mount_bind_sound-0.6.7-1.fc40.noarch.rpm
|
||||
|
||||
# Restart QM container (if already running)
|
||||
sudo systemctl daemon-reload
|
||||
sudo podman restart qm
|
||||
```
|
||||
|
||||
### Step 2: Identify Sound Cards
|
||||
|
||||
After installing the drop-in and restarting QM, you need to identify which sound card in the Linux system will be used in QM. If you're familiar with your sound card setup feel free to skip this step.
|
||||
|
||||
To list the sound cards available on your system (in our case, we will pick the number 1):
|
||||
|
||||
```bash
|
||||
cat /proc/asound/cards
|
||||
```
|
||||
|
||||
**Example Output**:
|
||||
|
||||
```bash
|
||||
0 [NVidia ]: HDA-Intel - HDA NVidia
|
||||
HDA NVidia at 0x9e000000 irq 17
|
||||
1 [sofhdadsp ]: sof-hda-dsp - sof-hda-dsp
|
||||
LENOVO-20Y5000QUS-ThinkPadX1ExtremeGen4i
|
||||
2 [USB ]: USB-Audio - USB Audio Device
|
||||
Generic USB Audio at usb-0000:00:14.0-5, full speed
|
||||
```
|
||||
|
||||
### Detecting Channels and Sample Rates
|
||||
|
||||
To list the supported number of channels and samples use `pactl` command:
|
||||
|
||||
```bash
|
||||
pactl list sinks | grep -i 48000 | uniq
|
||||
Sample Specification: s24-32le 2ch 48000Hz
|
||||
```
|
||||
|
||||
### Verify Sample Rate Support
|
||||
|
||||
To show the supported sample rates for a specific sound card codec, you can also inspect the codec details:
|
||||
|
||||
```bash
|
||||
cat /proc/asound/card1/codec#0 | grep -i rates
|
||||
```
|
||||
|
||||
This will output the supported sample rates for the codec associated with `card1`.
|
||||
|
||||
### Differentiating Between Cards
|
||||
|
||||
Accessing Card 1 (sof-hda-dsp)
|
||||
|
||||
```bash
|
||||
cat /proc/asound/cards | grep -A 1 '^ 1 '
|
||||
```
|
||||
|
||||
Accessing Card 2 (USB Audio Device)
|
||||
|
||||
```bash
|
||||
cat /proc/asound/cards | grep -A 1 '^ 2 '
|
||||
```
|
||||
|
||||
### Step 3: Testing audio inside QM
|
||||
|
||||
Inside QM, run the following command:
|
||||
|
||||
```bash
|
||||
podman exec -it qm bash
|
||||
bash-# podman ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
76dacaa9a89e quay.io/qm-images/audio:latest sleep infinity 7 hours ago Up 7 hours systemd-audio
|
||||
|
||||
|
||||
bash-# podman exec -it systemd-audio bash
|
||||
|
||||
Execute the audio test within the nested container, and the sound will be output through the physical speakers of your computer—or, in this case, the car's multimedia soundbox.
|
||||
bash-# speaker-test -D hw:1,0 -c 2 -r 48000
|
||||
```
|
||||
|
||||
Params:
|
||||
|
||||
```bash
|
||||
hw:1,0: sound card 1, device 0
|
||||
-c 2: two channels (stereo)
|
||||
-r 48000: sample rate of 48 kHz
|
||||
```
|
||||
|
||||
## QM sub-package OCI Hooks
|
||||
|
||||
The QM sub-package OCI Hooks provides dynamic device access management for containers through OCI runtime hooks. This subpackage includes essential hooks that enable secure and flexible device sharing between the host system and containers with robust error handling and comprehensive testing.
|
||||
|
||||
### Components
|
||||
|
||||
The `qm-oci-hooks` subpackage includes:
|
||||
|
||||
- **qm-device-manager**: Dynamic device mounting hook that provides access to various hardware devices based on container annotations
|
||||
- **wayland-client-devices**: Hook for GPU hardware acceleration access for Wayland client applications running as nested containers
|
||||
|
||||
### Supported Device Types
|
||||
|
||||
#### QM Device Manager
|
||||
|
||||
The `qm-device-manager` hook supports the following device types through container annotations:
|
||||
|
||||
| Device Type | Annotation | Devices Provided |
|
||||
|-------------|------------|------------------|
|
||||
| Audio | `org.containers.qm.device.audio=true` | `/dev/snd/*` (audio devices) |
|
||||
| Video | `org.containers.qm.device.video=true` | `/dev/video*`, `/dev/media*` (cameras, video devices) |
|
||||
| Input | `org.containers.qm.device.input=true` | `/dev/input/*` (keyboards, mice, touchpads) |
|
||||
| TTYs | `org.containers.qm.device.ttys=true` | `/dev/tty0-7` (virtual terminals) |
|
||||
| TTY USB | `org.containers.qm.device.ttyUSB=true` | `/dev/ttyUSB*` (USB TTY devices) |
|
||||
| DVB | `org.containers.qm.device.dvb=true` | `/dev/dvb/*` (digital TV devices) |
|
||||
| Radio | `org.containers.qm.device.radio=true` | `/dev/radio*` (radio devices) |
|
||||
| Multi-seat Support | `org.containers.qm.wayland.seat=seat0` | Input devices, render devices, and display devices associated with the specified systemd-logind seat |
|
||||
|
||||
#### Wayland Client Devices
|
||||
|
||||
The `wayland-client-devices` hook supports:
|
||||
|
||||
| Functionality | Annotation | Devices Provided |
|
||||
|---------------|------------|------------------|
|
||||
| GPU Acceleration | `org.containers.qm.wayland-client.gpu=true` | GPU render devices (`/dev/dri/render*`) for hardware acceleration |
|
||||
|
||||
### Features
|
||||
|
||||
- **Dynamic Device Discovery**: Automatically discovers and mounts available devices at container startup
|
||||
- **Annotation-Based Security**: Devices are only mounted when explicitly requested via annotations
|
||||
- **Multi-seat Support**: Enables proper device access in systemd-logind multi-seat environments
|
||||
- **Comprehensive Mock Device Support**: Full testing infrastructure with mock devices for all device types
|
||||
- **GPU Acceleration**: Provides hardware acceleration for Wayland client applications
|
||||
- **Comprehensive Logging**: All hooks provide detailed logging for monitoring and debugging:
|
||||
- Device Manager: `/var/log/qm-device-manager.log`
|
||||
- Wayland Client: `/var/log/qm-wayland-client-devices.log`
|
||||
- **Runtime Flexibility**: No system restart required when adding device access to new containers
|
||||
- **Lightweight Implementation**: Shell script-based hooks with minimal dependencies
|
||||
|
||||
### Building and Installing
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
make TARGETS=qm-oci-hooks subpackages
|
||||
sudo dnf install rpmbuild/RPMS/noarch/qm-oci-hooks-*.noarch.rpm
|
||||
```
|
||||
|
||||
### Usage Examples
|
||||
|
||||
#### Example 1: Serial communication with USB TTY devices
|
||||
|
||||
```bash
|
||||
# Create a container with access to all USB TTY devices
|
||||
cat > /etc/containers/systemd/serial-app.container << EOF
|
||||
[Unit]
|
||||
Description=Serial Communication Application
|
||||
|
||||
[Container]
|
||||
Image=my-serial-app:latest
|
||||
Annotation=org.containers.qm.device.ttyUSB=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example 2: Multi-seat Wayland compositor with session devices
|
||||
|
||||
```bash
|
||||
# Create a dropin for QM container to enable multi-seat support
|
||||
mkdir -p /etc/containers/systemd/qm.container.d/
|
||||
cat > /etc/containers/systemd/qm.container.d/wayland-seat.conf << EOF
|
||||
[Container]
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
EOF
|
||||
|
||||
# Create a Wayland compositor container that runs inside QM
|
||||
cat > /etc/qm/containers/systemd/wayland-compositor.container << EOF
|
||||
[Unit]
|
||||
Description=Wayland Compositor with Multi-seat Support
|
||||
|
||||
[Container]
|
||||
Image=wayland-compositor:latest
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
#### Example 3: Wayland client application with GPU acceleration
|
||||
|
||||
```bash
|
||||
# Create a Wayland client container with GPU hardware acceleration
|
||||
cat > /etc/qm/containers/systemd/gpu-app.container << EOF
|
||||
[Unit]
|
||||
Description=GPU-accelerated Application
|
||||
|
||||
[Container]
|
||||
Image=my-gpu-app:latest
|
||||
Annotation=org.containers.qm.wayland-client.gpu=true
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
EOF
|
||||
```
|
||||
|
||||
### Verification
|
||||
|
||||
To verify the hooks are installed and working:
|
||||
|
||||
```bash
|
||||
# Check all OCI hook installations
|
||||
ls -la /usr/share/containers/oci/hooks.d/
|
||||
|
||||
# Check hook executables
|
||||
ls -la /usr/libexec/oci/hooks.d/
|
||||
|
||||
# Verify hook JSON configurations are valid
|
||||
find /usr/share/containers/oci/hooks.d/ -name "*.json" -exec jq . {} \;
|
||||
|
||||
# View hook logs (all hooks provide comprehensive logging)
|
||||
tail -f /var/log/qm-device-manager.log
|
||||
tail -f /var/log/qm-wayland-client-devices.log
|
||||
|
||||
# Test device access with qm-device-manager
|
||||
podman exec -it my-audio-app ls -la /dev/snd/
|
||||
|
||||
# Test GPU access with wayland-client-devices (if applicable)
|
||||
podman exec -it gpu-app ls -la /dev/dri/
|
||||
|
||||
# Run hook tests (if source available)
|
||||
cd oci-hooks && tox -e all
|
||||
```
|
||||
|
||||
### Testing and Development
|
||||
|
||||
The OCI hooks include a comprehensive test suite for development and validation:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cd oci-hooks && tox -e all
|
||||
|
||||
# Run specific test categories
|
||||
tox -e unit # Unit tests only
|
||||
tox -e integration # Integration tests only
|
||||
tox -e performance # Performance tests only
|
||||
|
||||
# Run linting and formatting
|
||||
tox -e lint # Code linting
|
||||
tox -e format # Code formatting
|
||||
|
||||
# Test with mock devices (useful for CI environments)
|
||||
FORCE_MOCK_DEVICES=true tox -e unit -- -k "mock_devices"
|
||||
```
|
||||
|
||||
### OCI Hooks Specification and Documentation
|
||||
|
||||
The QM OCI hooks are implemented according to the [Open Container Initiative (OCI) Runtime Specification](https://github.com/opencontainers/runtime-spec).
|
||||
|
||||
#### OCI Hook Configuration Format
|
||||
|
||||
OCI hooks are configured using JSON files that define when and how the hooks should be executed. Each hook configuration follows the structure defined in the [OCI config schema](https://github.com/opencontainers/runtime-spec/blob/main/schema/config-schema.json).
|
||||
|
||||
**Key components of hook configuration:**
|
||||
|
||||
- **version**: OCI specification version (e.g., `"1.0.0"`)
|
||||
- **hook**: Object defining the hook executable and arguments
|
||||
- **when**: Object defining trigger conditions for the hook
|
||||
- **stages**: Array of lifecycle stages when the hook should run
|
||||
|
||||
#### Hook Lifecycle Stages
|
||||
|
||||
According to the [OCI POSIX Platform Hooks specification](https://github.com/opencontainers/runtime-spec/blob/main/config.md#posix-platform-hooks), hooks can be executed at three stages:
|
||||
|
||||
1. **prestart**: Hooks called after the container process is spawned, but before the user-supplied command is executed
|
||||
2. **poststart**: Hooks called after the user-supplied command is executed
|
||||
3. **poststop**: Hooks called after the container process is terminated
|
||||
|
||||
QM hooks execution stages:
|
||||
|
||||
- `qm-device-manager`: Runs during **prestart** to mount devices before the container starts
|
||||
- `wayland-client-devices`: Runs during **prestart** to provide GPU access before the container starts
|
||||
|
||||
#### Hook Input/Output Specification
|
||||
|
||||
OCI hooks receive container state information via **stdin** and communicate results via **stdout/stderr** and **exit codes**:
|
||||
|
||||
**Input (stdin)**: JSON object containing container state according to OCI spec:
|
||||
|
||||
```json
|
||||
{
|
||||
"ociVersion": "1.0.0",
|
||||
"id": "container-id",
|
||||
"status": "creating",
|
||||
"pid": 1234,
|
||||
"bundle": "/path/to/bundle",
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Output**:
|
||||
|
||||
- **Exit code 0**: Success
|
||||
- **Exit code non-zero**: Failure (container creation aborted)
|
||||
- **stderr**: Error messages and diagnostics
|
||||
- **stdout**: Hook output (typically empty for QM hooks)
|
||||
|
||||
#### Annotation Pattern Matching
|
||||
|
||||
QM hooks use regular expressions in their `when.annotations` configuration to match container annotations:
|
||||
|
||||
- `org\\.containers\\.qm\\.device\\.(audio|video|input|ttys|ttyUSB|dvb|radio)`: Matches device-specific annotations
|
||||
- `org\\.containers\\.qm\\.wayland\\.seat`: Matches Wayland seat annotations with any value
|
||||
- `org\\.containers\\.qm\\.wayland-client\\.gpu`: Matches GPU acceleration requests
|
||||
|
||||
#### Hook Installation Locations
|
||||
|
||||
QM hooks are installed in standard OCI locations:
|
||||
|
||||
- Hook executables: `/usr/libexec/oci/hooks.d/`
|
||||
- Hook configurations: `/usr/share/containers/oci/hooks.d/`
|
||||
- Hook libraries: `/usr/libexec/oci/lib/`
|
||||
|
||||
## QM sub-package ROS2
|
||||
|
||||
The QM sub-package ROS2 (a.k.a "The Robot Operating System" or middleware for robots) is widely used by open-source projects, enterprises, companies, edge env and government agencies, including NASA, to advance robotics and autonomous systems. Enabled by Quadlet in QM, ROS2 on top of QM provides a secure environment where robots can operate and communicate safely, benefiting from QM's "Freedom from Interference" frequently tested layer. This ensures robots can function without external interference, enhancing their reliability and security.
|
||||
|
||||
The types of robots compatible with this environment are extensive, ranging from medical devices and aerial drones to aqua drones and space rockets. ROS2 within QM supports high availability, meaning these robotic systems can maintain continuous operations, crucial for mission-critical and industrial applications. This versatility makes it ideal for environments that demand robust communication and operational safety, from healthcare and aerospace to underwater exploration and autonomous land vehicles.
|
||||
|
||||
How to test this env?
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
make TARGETS=ros2_rolling subpackages
|
||||
sudo dnf install rpmbuild/RPMS/noarch/qm_ros2_rolling-0.6.7-1.fc40.noarch.rpm -y
|
||||
sudo systemctl daemon-reload
|
||||
sudo podman restart qm # if you have qm already running
|
||||
|
||||
Testing using talker and listener examples
|
||||
$host> sudo podman exec -it qm bash
|
||||
QM> . /opt/ros/jazzy/setup.bash # always replace jazz with the image ROS distro
|
||||
QM> ros2 run demo_nodes_cpp talker &
|
||||
QM> ros2 run demo_nodes_cpp listener
|
||||
```
|
||||
|
||||
## QM sub-package KVM
|
||||
|
||||
The QM sub-package KVM includes drop-in configuration that enables the integration of Kernel-based Virtual Machine (KVM) management into the QM (Quality Management) container environment.
|
||||
This configuration allows users to pull containerized kvm from [qm-images-repo](https://quay.io/repository/qm-images/kvm) and run it inside QM
|
||||
|
||||
There is also kvm.container which is installed as a service.
|
||||
|
||||
Below example step by step:
|
||||
|
||||
Step 1: clone QM repo, create rpm.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/containers/qm.git && cd qm
|
||||
make TARGETS=kvm subpackages
|
||||
```
|
||||
|
||||
Step 2: copy rpm to running machine
|
||||
|
||||
```bash
|
||||
scp -oStrictHostKeyChecking=no -oUserKnownHostsFile=/dev/null -P 2222 rpmbuild/RPMS/noarch/qm-kvm-0.7.4-1.fc41.noarch.rpm root@127.0.0.1:/root/
|
||||
```
|
||||
|
||||
Step 3: ssh machine install and verify
|
||||
|
||||
```bash
|
||||
sudo dnf install ./qm-kvm-0.7.4-1.fc41.noarch.rpm
|
||||
sudo systemctl restart qm # if you have qm already running
|
||||
```
|
||||
|
||||
Step 4: verify, configuration exist
|
||||
|
||||
```bash
|
||||
ls -ltr /etc/containers/systemd/qm.container.d/
|
||||
total 12
|
||||
-rw-r--r--. 1 root root 34 Jan 1 1970 publish-port.conf
|
||||
-rw-r--r--. 1 root root 139 Jul 21 2023 qm_dropin_mount_bind_kvm.conf
|
||||
|
||||
ls -ltr /etc/qm/containers/systemd/
|
||||
total 12
|
||||
-rw-r--r--. 1 root root 91 Jan 1 1970 nginx.container
|
||||
-rw-r--r--. 1 root root 188 Jul 21 2023 kvm.container
|
||||
|
||||
[root@localhost ~]# podman exec qm systemctl is-active kvm
|
||||
active
|
||||
|
||||
[root@localhost ~]# podman exec -it qm sh
|
||||
sh-5.1# ssh fedora@localhost -p 2226
|
||||
[fedora@ibm-p8-kvm-03-guest-02 ~]$ grep ^NAME /etc/os-release
|
||||
NAME="Fedora Linux"
|
||||
```
|
||||
|
||||
### AutoSD install
|
||||
|
||||
Some notes related to installing qm on ostree AutoSD image
|
||||
|
||||
1. Check /var/qm size is larger then 1.5G
|
||||
2. Installing in ostree images with dnf command, requires running rpm-ostree usroverlay
|
||||
|
||||
In case using aib schema to build your image, verify adding the following to build command
|
||||
|
||||
```bash
|
||||
--define 'extra_rpms=["audit","dnf","python3-gobject"] qm_varpart_relative_size=0.5'
|
||||
```
|
|
@ -0,0 +1,17 @@
|
|||
## Installing QM
|
||||
|
||||
The first step to getting started with QM is installation.
|
||||
|
||||
Fedora or CentOS:
|
||||
On Fedora and CentOS-Stream systems (with EPEL repository enabled), QM can be directly installed via:
|
||||
|
||||
```bash
|
||||
dnf install qm
|
||||
```
|
||||
|
||||
## RPM Mirrors
|
||||
|
||||
Looking for a specific version of QM?
|
||||
Search in the mirrors list below.
|
||||
|
||||
[CentOS Automotive SIG - qm package - noarch](https://mirror.stream.centos.org/SIGs/9-stream/automotive/aarch64/packages-main/Packages/q/)
|
|
@ -0,0 +1,24 @@
|
|||
# Virtualization: Android container with Quadlet
|
||||
|
||||
This is an example of an Android container running on top of kvm using quadlet and Wayland:
|
||||
|
||||
```console
|
||||
$ cat ~/.config/containers/systemd/android.container
|
||||
|
||||
[Service]
|
||||
Environment=WAYLAND_DISPLAY=wayland-0
|
||||
|
||||
[Container]
|
||||
AddDevice=/dev/dri/renderD128
|
||||
AddDevice=/dev/kvm
|
||||
ContainerName=android
|
||||
NoNewPrivileges=true
|
||||
DropCapability=all
|
||||
Environment=PULSE_SERVER=%t/pulse/native
|
||||
Environment=WAYLAND_DISPLAY=${WAYLAND_DISPLAY}
|
||||
Environment=XDG_RUNTIME_DIR=%t
|
||||
Image=quay.io/slopezpa/qemu-aaos
|
||||
PodmanArgs=--shm-size=5g
|
||||
SecurityLabelDisable=true
|
||||
Volume=%t:%t
|
||||
```
|
|
@ -0,0 +1,84 @@
|
|||
# Using network modes with QM
|
||||
|
||||
## Basics: Network Modes in Podman
|
||||
|
||||
When running a container with Podman, you can specify the network mode using the `--network` flag. Two common options are `host` and `private`.
|
||||
|
||||
### Network=host
|
||||
|
||||
If you set `--network=host`, the container will use the host's network stack. This means the container will share the same network namespace as the host, and will be able to access the host's network interfaces, IP addresses, and ports.
|
||||
|
||||
In this mode, the container is not isolated from the host's network, and can potentially access sensitive network resources. This can be useful for certain use cases, such as running a container that needs to access a specific network interface or port on the host.
|
||||
|
||||
### Network=private (default)
|
||||
|
||||
By default, Podman uses the `private` network mode. This means that the container will have its own isolated network namespace, and will not be able to access the host's network interfaces, IP addresses, or ports.
|
||||
|
||||
In this mode, the container is isolated from the host's network, and can only communicate with other containers on the same network. This provides a higher level of security, as the container is not able to access sensitive network resources on the host.
|
||||
|
||||
### Security Implications
|
||||
|
||||
The reason `private` is the default network mode is due to security concerns. By isolating the container's network namespace, Podman prevents the container from accessing sensitive network resources on the host, such as:
|
||||
|
||||
* Host's network interfaces and IP addresses
|
||||
* Host's ports and services
|
||||
* Other containers on the host
|
||||
|
||||
This helps to prevent potential security vulnerabilities, such as:
|
||||
|
||||
* Container escape: a container accessing sensitive resources on the host
|
||||
* Lateral movement: a container accessing other containers on the host
|
||||
|
||||
### Example
|
||||
|
||||
To illustrate the difference, consider the following example:
|
||||
|
||||
```bash
|
||||
# Run a container with network=host
|
||||
podman run -it --network=host fedora /bin/bash
|
||||
|
||||
# Run a container with network=private (default)
|
||||
podman run -it --network=private fedora /bin/bash
|
||||
```
|
||||
|
||||
In the first example, the container will share the host's network namespace, while in the second example, the container will have its own isolated network namespace.
|
||||
|
||||
For more information, see the [Podman Networking Tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/basic_networking.md).
|
||||
|
||||
For network modes configuration example using quadlets, see [Quadlet Network Example](https://github.com/containers/qm/blob/main/docs/quadlet-examples/network/README.md).
|
||||
|
||||
## Quadlet example running host and private networks
|
||||
|
||||
Here is an example of running a network-test container using quadlets for both --network=host and --network=private. You should place this file either in /usr/share/containers/systemd/ or /etc/containers/systemd/
|
||||
|
||||
```console
|
||||
/usr/share/containers/systemd/
|
||||
/etc/containers/systemd/
|
||||
```
|
||||
|
||||
For rootless users:
|
||||
|
||||
```console
|
||||
$HOME/.config/containers/systemd/
|
||||
|
||||
```
|
||||
|
||||
Host Network
|
||||
|
||||
```console
|
||||
# network-test.container
|
||||
[Container]
|
||||
ContainerName=network-test
|
||||
Image=localhost/local-audio-image
|
||||
Network=host
|
||||
```
|
||||
|
||||
Private Network
|
||||
|
||||
```console
|
||||
# network-test.container
|
||||
[Container]
|
||||
ContainerName=network-test
|
||||
Image=localhost/local-audio-image
|
||||
Network=private
|
||||
```
|
|
@ -0,0 +1,24 @@
|
|||
# Changing variables in qm containers.conf
|
||||
|
||||
The `container.conf` file needs to be modified to allow pulling images larger than 1G from the repository on OStree images.
|
||||
|
||||
## Update container image_copy_tmp_dir if the image is an OStree
|
||||
|
||||
1. Create /var/qm/tmp.dir or differently named directory on host.
|
||||
2. Create /etc/qm/containers/containers.conf.d or differently named directory on host.
|
||||
3. Create and edit /etc/qm/containers/containers.conf.d/qm_image_tmp_dir.conf or differently named *.conf file and add the following lines:
|
||||
[engine]
|
||||
image_copy_tmp_dir="/var/tmp.dir"
|
||||
|
||||
## Sample code should look like this
|
||||
|
||||
<https://github.com/nsednev/qm/blob/3bbe302791ea5d0f271a1cc96ed6bce4d4b99de2/tests/ffi/common/prepare.sh#L76-L79>
|
||||
|
||||
By default image_copy_tmp_dir="/var/tmp".
|
||||
Changing its default value to /var/tmp.dir will allow the container to pull images larger than 1G.
|
||||
|
||||
This is a work around and it should not be used constantly.
|
||||
|
||||
## Expected result
|
||||
|
||||
Containers on host will be able to pull images larger than 1G.
|
|
@ -0,0 +1,11 @@
|
|||
# QM
|
||||
|
||||
## QM is a containerized environment for running Functional Safety QM (Quality Management) software
|
||||
|
||||
The qm package sets up an isolated runtime environment for non-critical processes, managed through container tools and systemd. It is designed to ensure that these processes do not interfere with the host system, making it ideal for scenarios such as ASIL (Automotive Safety Integrity Level) separation.
|
||||
|
||||
QM runs as an exploded container—a persistent, containerized root filesystem mounted under /var/lib/qm/rootfs. It operates with its own instance of systemd, effectively creating a nested user space within its dedicated disk partition. This setup allows the system to isolate and control resource usage via cgroups, namespaces, and security constraints.
|
||||
|
||||
System-level tooling like Podman and systemd inside QM are fully independent from the host, so even container commands themselves are sandboxed. The environment is provisioned using Podman and configured with quadlet units, which streamline setup and lifecycle management.
|
||||
|
||||
Software installed inside the QM root is automatically isolated from the host. Developers can further segment workloads by using container tools inside QM to manage additional levels of containment for processes requiring extra isolation.
|
|
@ -0,0 +1,138 @@
|
|||
# Setting up IPC
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Setting up IPC](#setting-up-ipc)
|
||||
|
||||
1.1 [ASIL to QM IPC](#example-asil-to-qm-app)
|
||||
- [`/etc/systemd/system/ipc_server.socket`](#etcsystemdsystemipc_serversocket)
|
||||
- [`/etc/qm/containers/systemd/ipc_client.container`](#etcqmcontainerssystemdipc_clientcontainer)
|
||||
- [`/etc/containers/systemd/ipc_server.container`](#etccontainerssystemdipc_servercontainer)
|
||||
- [`/etc/containers/systemd/qm.container.d/10-extra-volume.conf`](#etccontainerssystemdqmcontainerd10-extra-volumeconf)
|
||||
|
||||
1.2 [QM to QM IPC](#example-qm-to-qm-app)
|
||||
- [`/etc/qm/containers/systemd/ipc_client.container`](#etcqmcontainerssystemdipc_clientcontainer-1)
|
||||
- [`/etc/qm/containers/systemd/ipc_server.container`](#etcqmcontainerssystemdipc_servercontainer)
|
||||
|
||||
---
|
||||
|
||||
In systems where **Automotive Safety Integrity Level (ASIL)** and **Quality Management (QM)**
|
||||
components coexist, strict separation is enforced to maintain safety and security boundaries via
|
||||
**SELinux (Security-Enhanced Linux)**, which labels processes and files with security contexts
|
||||
to control their interactions.
|
||||
|
||||
**IPC (Inter-Process Communication)** between ASIL and QM components must be tightly controlled.
|
||||
To comply with SELinux policies and avoid permission denials, any socket-based communication
|
||||
between ASIL and QM domains should be established in the dedicated directory such as /run/ipc
|
||||
with ipc_var_run_t file context. It serves as a secure bridge for cross-domain communication
|
||||
while maintaining SELinux isolation.
|
||||
|
||||
On the other hand, **IPC between QM services** (e.g., two services or containers within the same QM domain)
|
||||
can occur as well. Since these components share the same SELinux type and context, they are allowed to
|
||||
communicate using standard Unix domain sockets located in /run. This approach simplifies internal QM
|
||||
communication relies on container IPC without compromising the system's overall security posture. Such communication can be
|
||||
orchestrated also using container orchestration patterns like **.pod (Podman pod definitions)** or
|
||||
**.kube (Kubernetes pod manifests)**, which group related services in shared namespaces to support efficient
|
||||
IPC within the same trust boundary.
|
||||
|
||||
## Example ASIL to QM app
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
### /etc/systemd/system/ipc_server.socket
|
||||
|
||||
```console
|
||||
[Unit]
|
||||
Description=IPC Server Socket for asil-to-qm
|
||||
[Socket]
|
||||
ListenStream=%t/ipc/ipc.socket
|
||||
SELinuxContextFromNet=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=sockets.target
|
||||
```
|
||||
|
||||
### /etc/qm/containers/systemd/ipc_client.container
|
||||
|
||||
``` console
|
||||
[Unit]
|
||||
Description=Demo client service container (asil-to-qm)
|
||||
[Container]
|
||||
Image=quay.io/username/ipc-demo/ipc_client:latest
|
||||
Network=none
|
||||
Environment=SOCKET_PATH=/run/ipc/ipc.socket
|
||||
Volume=/run/ipc/:/run/ipc/
|
||||
SecurityLabelType=qm_container_ipc_t
|
||||
[Service]
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### /etc/containers/systemd/ipc_server.container
|
||||
|
||||
```console
|
||||
[Unit]
|
||||
Description=Demo server service container (asil-to-qm)
|
||||
Requires=ipc_server.socket
|
||||
After=ipc_server.socket
|
||||
[Container]
|
||||
Image=quay.io/username/ipc-demo/ipc_server
|
||||
Network=none
|
||||
Environment=SOCKET_PATH=/run/ipc/ipc.socket
|
||||
Volume=/run/ipc/:/run/ipc/
|
||||
SecurityLabelType=ipc_t
|
||||
[Service]
|
||||
Restart=always
|
||||
Type=notify
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### /etc/containers/systemd/qm.container.d/10-extra-volume.conf
|
||||
|
||||
```console
|
||||
[Unit]
|
||||
Requires=ipc_server
|
||||
|
||||
[Container]
|
||||
Volume=/run/ipc/:/run/ipc/
|
||||
```
|
||||
|
||||
## Example QM to QM app
|
||||
|
||||
### /etc/qm/containers/systemd/ipc_client.container
|
||||
|
||||
```console
|
||||
[Unit]
|
||||
Description=Demo client service container
|
||||
Requires=ipc_server.service
|
||||
After=ipc_server.service
|
||||
|
||||
[Container]
|
||||
Image=quay.io/username/ipc-demo/ipc_client:latest
|
||||
Network=none
|
||||
Volume=/run/ipc.socket:/run/ipc.socket
|
||||
SecurityLabelLevel=s0:c1,c2
|
||||
[Service]
|
||||
Restart=always
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### /etc/qm/containers/systemd/ipc_server.container
|
||||
|
||||
```console
|
||||
[Unit]
|
||||
Description=Demo server service container
|
||||
[Container]
|
||||
Image=quay.io/username/ipc-demo/ipc_server:latest
|
||||
Network=none
|
||||
Volume=/run/:/run/
|
||||
SecurityLabelLevel=s0:c1,c2
|
||||
[Service]
|
||||
Restart=always
|
||||
Type=notify
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
<!-- markdownlint-disable MD024 -->
|
|
@ -0,0 +1,6 @@
|
|||
# Additional Resources
|
||||
|
||||
- [Paving the Way for Uninterrupted Car Operations - DevConf Boston 2024](https://www.youtube.com/watch?v=jTrLqpw7E6Q)
|
||||
- [Security - Sample Risk Analysis according to ISO26262](https://www.youtube.com/watch?v=jTrLqpw7E6Q&t=1268s)
|
||||
- [ASIL and QM - Simulation and Service Monitoring using bluechi and podman](https://www.youtube.com/watch?v=jTrLqpw7E6Q&t=1680s)
|
||||
- [Containers in a Car - DevConf.CZ 2023](https://www.youtube.com/watch?v=FPxka5uDA_4)
|
|
@ -0,0 +1,262 @@
|
|||
# Using QM
|
||||
|
||||
This section describes how to interact with QM.
|
||||
|
||||
## Installing software inside QM partition
|
||||
|
||||
```bash
|
||||
dnf --installroot /usr/lib/qm/rootfs/ install vim -y
|
||||
```
|
||||
|
||||
## Removing software inside QM partition
|
||||
|
||||
```bash
|
||||
dnf --installroot /usr/lib/qm/rootfs/ remove vim -y
|
||||
```
|
||||
|
||||
## Copying files to QM partition
|
||||
|
||||
Please note: This process is only applicable for regular images.
|
||||
OSTree images are read-only, and any files must be included during the build process.
|
||||
|
||||
Once this is understood, proceed by executing the following command on the host after
|
||||
the QM package has been installed.
|
||||
|
||||
```bash
|
||||
#host> cp file_to_be_copied /usr/lib/qm/rootfs/root
|
||||
#host> podman exec -it qm bash
|
||||
bash-5.1> ls /root
|
||||
file_to_be_copied
|
||||
```
|
||||
|
||||
## Listing QM service
|
||||
|
||||
```bash
|
||||
[root@localhost ~]# systemctl status qm -l
|
||||
● qm.service
|
||||
Loaded: loaded (/usr/share/containers/systemd/qm.container; generated)
|
||||
Active: active (running) since Sun 2024-04-28 22:12:28 UTC; 12s
|
||||
ago
|
||||
Main PID: 354 (conmon)
|
||||
Tasks: 7 (limit: 7772)
|
||||
Memory: 82.1M (swap max: 0B)
|
||||
CPU: 945ms
|
||||
CGroup: /qm.service
|
||||
├─libpod-payload-a83253ae278d7394cb38e975535590d71de90a41157b547040
|
||||
4abd6311fd8cca
|
||||
│ ├─init.scope
|
||||
│ │ └─356 /sbin/init
|
||||
│ └─system.slice
|
||||
│ ├─bluechi-agent.service
|
||||
│ │ └─396 /usr/libexec/bluechi-agent
|
||||
│ ├─dbus-broker.service
|
||||
│ │ ├─399 /usr/bin/dbus-broker-launch --scope system
|
||||
--audit
|
||||
│ │ └─401 dbus-broker --log 4 --controller 9 --machin
|
||||
e-id a83253ae278d7394cb38e975535590d7 --max-bytes 536870912 --max-fds 4096 --max
|
||||
-matches 16384 --audit
|
||||
```
|
||||
|
||||
## List QM container via podman
|
||||
|
||||
```console
|
||||
# podman ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
a83253ae278d /sbin/init 38 seconds ago Up 38 seconds qm
|
||||
```
|
||||
|
||||
## Extend QM quadlet managed by podman
|
||||
|
||||
QM quadlet file is shipped through rpm, refer the following file.
|
||||
qm.container which is installed to /usr/share/containers/systemd/qm.container
|
||||
Please refer `man quadlet` for the supported value and how to.
|
||||
|
||||
In case a change needed in quadlet file, do not update systemd/qm.container file
|
||||
As per `man quadlet` do the following:
|
||||
|
||||
```console
|
||||
if ! test -e /etc/containers/systemd/qm.container.d ; then
|
||||
mkdir -p /etc/containers/systemd/qm.container.d
|
||||
fi
|
||||
cat > "/etc/containers/systemd/qm.container.d/expose-dev.conf" <<EOF
|
||||
[Container]
|
||||
# Expose host device /dev/net/tun
|
||||
AddDevice=-/dev/net/tun
|
||||
# In case parameter override needed, add empty value before the required key
|
||||
Unmask=
|
||||
Unmask=ALL
|
||||
EOF
|
||||
```
|
||||
|
||||
To verify the result use the following command:
|
||||
|
||||
```console
|
||||
/usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
||||
```
|
||||
|
||||
Once the result is satisfied, apply the following
|
||||
|
||||
```console
|
||||
systemctl daemon-reload
|
||||
systemctl restart qm
|
||||
systemctl is-active qm
|
||||
active
|
||||
```
|
||||
|
||||
## Managing CPU usage
|
||||
|
||||
Using the steps below, it's possible to manage CPU usage of the `qm.service` by modifying service attributes and utilizing drop-in files.
|
||||
|
||||
### Setting the CPUWeight attribute
|
||||
|
||||
Modifying the `CPUWeight` attribute affects the priority of the `qm.service`. A higher value prioritizes the service, while a lower value deprioritizes it.
|
||||
|
||||
Inspect the current CPUWeight value:
|
||||
|
||||
```bash
|
||||
systemctl show -p CPUWeight qm.service
|
||||
```
|
||||
|
||||
Set the CPUWeight value:
|
||||
|
||||
```bash
|
||||
systemctl set-property qm.service CPUWeight=500
|
||||
```
|
||||
|
||||
### Limiting CPUQuota
|
||||
|
||||
It's also possible to limit the percentage of the CPU allocated to the `qm.service` by defining `CPUQuota`. The percentage specifies how much CPU time the unit shall get at maximum, relative to the total CPU time available on one CPU.
|
||||
|
||||
Inspect the current `CPUQuota` value via the `CPUQuotaPerSecUSec` property:
|
||||
|
||||
```bash
|
||||
systemctl show -p CPUQuotaPerSecUSec qm.service
|
||||
```
|
||||
|
||||
Set the `CPUQuota` value of `qm.service` on the host using:
|
||||
|
||||
```bash
|
||||
systemctl set-property qm.service CPUQuota=50%
|
||||
```
|
||||
|
||||
Verify the `CPUQuota` drop in file has been created using the command below.
|
||||
|
||||
```bash
|
||||
systemctl show qm.service | grep "DropInPath"
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```bash
|
||||
DropInPaths=/usr/lib/systemd/system/service.d/10-timeout-abort.conf /etc/systemd/system.control/qm.service.d/50-CPUQuota.conf
|
||||
```
|
||||
|
||||
To test maxing out CPU usage and then inspect using the `top` command, follow these steps:
|
||||
|
||||
- Set the `CPUQuota` value of `qm.service` on the host using:
|
||||
|
||||
```bash
|
||||
systemctl set-property qm.service CPUQuota=50%
|
||||
```
|
||||
|
||||
- Execute this command to stress the CPU for 30 seconds:
|
||||
|
||||
```bash
|
||||
podman exec qm timeout 30 dd if=/dev/zero of=/dev/null
|
||||
```
|
||||
|
||||
- Observe the limited CPU consumption from the `qm.service`, as shown in the output of the command below:
|
||||
|
||||
```bash
|
||||
top | head
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```bash
|
||||
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
|
||||
1213867 root 20 0 2600 1528 1528 R 50.0 0.0 4:15.21 dd
|
||||
3471 user 20 0 455124 7568 6492 S 8.3 0.0 1:43.64 ibus-en+
|
||||
1 root 20 0 65576 37904 11116 S 0.0 0.1 0:40.00 systemd
|
||||
```
|
||||
|
||||
## Connecting to QM container via podman
|
||||
|
||||
```console
|
||||
# podman exec -it qm bash
|
||||
bash-5.1#
|
||||
```
|
||||
|
||||
## SSH guest CentOS Automotive Stream Distro
|
||||
|
||||
Make sure the CentOS Automotive Stream Distro Virtual Machine/Container is running with SSHD enabled
|
||||
and permits ssh connection from root user.
|
||||
|
||||
Add **PermitRootLogin yes** into **sshd_config**
|
||||
|
||||
```bash
|
||||
host> vi /etc/ssh/sshd_config
|
||||
```
|
||||
|
||||
Restart systemctl restart sshd
|
||||
|
||||
```bash
|
||||
host> systemctl restart sshd
|
||||
```
|
||||
|
||||
Find the port the ssh is listening in the VM
|
||||
|
||||
```bash
|
||||
host> netstat -na |more # Locate the port (2222 or 2223, etc)
|
||||
```
|
||||
|
||||
Example connecting from the terminal to the Virtual Machine:
|
||||
|
||||
```bash
|
||||
connect-to-VM-via-SSH> ssh root@127.0.0.1 \
|
||||
-p 2222 \
|
||||
-oStrictHostKeyChecking=no \
|
||||
-oUserKnownHostsFile=/dev/null
|
||||
```
|
||||
|
||||
## Check if HOST and Container are using different network namespace
|
||||
|
||||
### HOST
|
||||
|
||||
```console
|
||||
[root@localhost ~]# ls -l /proc/self/ns/net
|
||||
lrwxrwxrwx. 1 root root 0 May 1 04:33 /proc/self/ns/net -> 'net:[4026531840]'
|
||||
```
|
||||
|
||||
### QM
|
||||
|
||||
```console
|
||||
bash-5.1# ls -l /proc/self/ns/net
|
||||
lrwxrwxrwx. 1 root root 0 May 1 04:33 /proc/self/ns/net -> 'net:[4026532287]'
|
||||
```
|
||||
|
||||
## Debugging with podman in QM
|
||||
|
||||
```console
|
||||
bash-5.1# podman --root /usr/share/containers/storage pull alpine
|
||||
Error: creating runtime static files directory "/usr/share/containers/storage/libpod":
|
||||
mkdir /usr/share/containers/storage: read-only file system
|
||||
```
|
||||
|
||||
## Debugging with quadlet
|
||||
|
||||
Imagine a situation where you have a Quadlet container inside QM that isn't starting, and you're unsure why. The best approach is to log into the QM, run the ```quadlet --dryrun``` command, and analyze what's happening. Here's how you can troubleshoot the issue step by step.
|
||||
|
||||
```bash
|
||||
$ sudo podman exec -it qm bash
|
||||
bash-5.1# cd /etc/containers/systemd/
|
||||
bash-5.1# ls
|
||||
ros2.container
|
||||
|
||||
bash-5.1# /usr/libexec/podman/quadlet --dryrun
|
||||
quadlet-generator[1068]: Loading source unit file /etc/containers/systemd/ros2.container
|
||||
quadlet-generator[1068]: converting "ros2.container": unsupported key 'Command' in group 'Container' in /etc/containers/systemd/ros2.container
|
||||
bash-5.1#
|
||||
```
|
||||
|
||||
As you can see above, the error occurs because the Quadlet is attempting to use an unsupported key from the Service section in the Container group. Removing the unsupported key ```Command``` from ```ros2.container``` and then reloading or restarting the service should resolve the issue.
|
|
@ -0,0 +1,38 @@
|
|||
site_name: QM Documentation
|
||||
|
||||
repo_url: https://github.com/containers/qm
|
||||
repo_name: qm
|
||||
edit_uri: blob/main/docs/docs/
|
||||
|
||||
copyright: Copyright Contributors to the QM project
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: getting_started.md
|
||||
- Using QM: usage.md
|
||||
- Setting up IPC: ipc.md
|
||||
- How To:
|
||||
- Android container: how_tos/android.md
|
||||
- Using network modes with QM: how_tos/network.md
|
||||
- Changing variables in QM: how_tos/qm_variables.md
|
||||
- Experimental:
|
||||
- QM Subpackages: experimental/subpackages.md
|
||||
- Additional resources: resources.md
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- content.code.copy
|
||||
- navigation.indexes
|
||||
|
||||
markdown_extensions:
|
||||
- toc:
|
||||
permalink: True
|
||||
- sane_lists
|
||||
- smarty
|
||||
- admonition
|
||||
- pymdownx.snippets:
|
||||
base_path: ["docs"]
|
||||
check_paths: True
|
||||
- pymdownx.superfences:
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
|
@ -0,0 +1,3 @@
|
|||
mkdocs>=1.3.0
|
||||
mkdocs-material>=9.3.1
|
||||
pymdown-extensions>=10.0.1
|
|
@ -0,0 +1,5 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/kvm from host to container
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/net/tun
|
||||
AddDevice=-/dev/kvm
|
|
@ -0,0 +1,42 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/snd from host to container
|
||||
#
|
||||
# The ALSA sound architecture uses /dev/snd to represent sound hardware devices
|
||||
# as special files in the filesystem. These files are typically accessed by
|
||||
# user-space programs (like media players, audio recorders, or mixers) to
|
||||
# interact with the sound hardware. Applications do not usually access these
|
||||
# files directly. Instead, they use higher-level libraries (like PulseAudio,
|
||||
# PipeWire, or ALSA's own libraries) to interface with the devices.
|
||||
#
|
||||
# +-------------------------------------------------------------+
|
||||
# | User-Space Applications |
|
||||
# |-------------------------------------------------------------|
|
||||
# | Media Players | Audio Recorders | Audio Mixers |
|
||||
# +-------------------------------------------------------------+
|
||||
# | | |
|
||||
# v v v
|
||||
# +-------------------------------------------------------------+
|
||||
# | High-Level Audio Libraries (User Space) |
|
||||
# |-------------------------------------------------------------|
|
||||
# | ALSA Libraries | PulseAudio | PipeWire |
|
||||
# +-------------------------------------------------------------+
|
||||
# | | |
|
||||
# v v v
|
||||
# +-------------------------------------------------------------+
|
||||
# | /dev/snd (Special Files) |
|
||||
# | ALSA Kernel Driver exposes sound devices as /dev/snd |
|
||||
# +-------------------------------------------------------------+
|
||||
# | | |
|
||||
# v v v
|
||||
# +-------------------------------------------------------------+
|
||||
# | ALSA Kernel Sound Drivers (Hardware) |
|
||||
# | Interfacing with Sound Cards and Audio Devices |
|
||||
# +-------------------------------------------------------------+
|
||||
# | | |
|
||||
# v v v
|
||||
# +-------------------------------------------------------------+
|
||||
# | Sound Hardware (Speakers, Mics) |
|
||||
# +-------------------------------------------------------------+
|
||||
#
|
||||
# qm_dropin_mount_bind_snd.conf
|
||||
[Container]
|
||||
Annotation=org.containers.qm.device.audio=true
|
|
@ -0,0 +1,60 @@
|
|||
# Drop-in configuration for Podman to mount bind /dev/videoX cameras devices
|
||||
#
|
||||
# In a typical vehicle system, cameras are connected to the car's onboard computer via a CAN bus
|
||||
# (Controller Area Network), which transmits signals from the cameras to the car’s system for real-time
|
||||
# processing. This is commonly used for functions like parking assistance, lane-keeping, or 360-degree
|
||||
# surround-view systems.
|
||||
#
|
||||
# However, it's possible to create a simulation environment using traditional hardware and open-source
|
||||
# software, eliminating the need for actual car cameras or CAN bus integration. By using open-source
|
||||
# tools like Podman containers and video processing libraries (such as GStreamer or FFmpeg), virtual
|
||||
# cameras can be simulated. These tools allow developers to simulate the video signals typically
|
||||
# produced by physical car cameras and routed through the CAN bus.
|
||||
#
|
||||
# In this setup, virtual devices (e.g., /dev/video0, /dev/video1) are mounted into a container and can
|
||||
# provide simulated video streams that mimic real camera feeds. This allows automotive software to be
|
||||
# developed and tested in a controlled environment, replicating the behavior of car cameras without
|
||||
# needing access to the physical hardware or CAN bus.
|
||||
#
|
||||
# "/dev/video0:/dev/video0", # Front camera
|
||||
# "/dev/video1:/dev/video1", # Back camera
|
||||
#
|
||||
# "/dev/video2:/dev/video2", # Power side view mirror camera (Right)
|
||||
# "/dev/video3:/dev/video3", # Power side view mirror camera (Left)
|
||||
#
|
||||
# "/dev/video4:/dev/video4", # side car camera (Right)
|
||||
# "/dev/video5:/dev/video5" # side car camera (Left)
|
||||
#
|
||||
# Camera System Layout (Top-Down View)
|
||||
#
|
||||
# ┌─────────────────────────────┐
|
||||
# │ /dev/video0 │
|
||||
# │ (Front Camera) │
|
||||
# │ Primary forward-view camera│
|
||||
# └────────────┬────────────────┘
|
||||
# │
|
||||
# ┌─────────────────────┴────────────────────────────────┐
|
||||
# │ Vehicle Body (Top View) │
|
||||
# │ │
|
||||
# │ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
# │ │ /dev/video2 │ │ /dev/video3 │ │
|
||||
# │ │ (Right Mirror) │ │ (Left Mirror) │ │
|
||||
# │ │ Secondary camera│ │ Secondary camera│ │
|
||||
# │ └─────────────────┘ └─────────────────┘ │
|
||||
# │ │
|
||||
# │ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
# │ │ /dev/video4 │ │ /dev/video5 │ │
|
||||
# │ │ (Right Side) │ │ (Left Side) │ │
|
||||
# │ │ Side-view camera│ │ Side-view camera│ │
|
||||
# │ └─────────────────┘ └─────────────────┘ │
|
||||
# └──────────────────────────────────────────────────────┘
|
||||
# │
|
||||
# ┌─────────────────────────────┐
|
||||
# │ /dev/video1 │
|
||||
# │ (Rear Camera) │
|
||||
# │ Primary rear-view camera │
|
||||
# └─────────────────────────────┘
|
||||
#
|
||||
#
|
||||
[Container]
|
||||
AddDevice=-/dev/video0
|
|
@ -0,0 +1,4 @@
|
|||
# Drop-in configuration for Podman to mount bind tty from host to container
|
||||
#
|
||||
[Container]
|
||||
Annotation=org.containers.qm.device.ttys=true
|
|
@ -0,0 +1,13 @@
|
|||
# This configuration file was generated by qm-dropin sub-package:
|
||||
# - qm-dropin-img-tempdir
|
||||
#
|
||||
# It configures the temporary directory used by the container engine
|
||||
# for image copy operations. This setting is useful for performance
|
||||
# optimizations or managing disk space.
|
||||
#
|
||||
# How it works?
|
||||
#=================
|
||||
# [engine] <- section
|
||||
# image_copy_tmp_dir <- Specifies the directory for temporary image storage.
|
||||
[engine]
|
||||
image_copy_tmp_dir="/var/tmp.dir"
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
# Common Utility Library for OCI Hooks
|
||||
# This file provides shared utility functions for all OCI hooks.
|
||||
|
||||
# Common logging function for OCI hooks
|
||||
log() {
|
||||
local level="$1"
|
||||
shift
|
||||
local message
|
||||
message="$(date '+%Y-%m-%d %H:%M:%S') - ${HOOK_NAME:-oci-hook} - $level - $*"
|
||||
|
||||
# Write to log file if LOGFILE is set
|
||||
if [[ -n "${LOGFILE:-}" ]]; then
|
||||
echo "$message" >>"$LOGFILE"
|
||||
fi
|
||||
|
||||
# Also write errors to stderr
|
||||
if [[ "$level" == "ERROR" ]]; then
|
||||
echo "$message" >&2
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
#!/bin/bash
|
||||
# Device Support Library for OCI Hooks
|
||||
# This file provides standard device discovery functionality for OCI hooks.
|
||||
|
||||
# Source common utilities
|
||||
# shellcheck source=./common.sh disable=SC1091
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# Device discovery function using standard filesystem operations
|
||||
discover_devices() {
|
||||
local pattern="$1"
|
||||
local device_type="$2"
|
||||
|
||||
# Extract directory path from pattern to check existence first
|
||||
local dir_path=""
|
||||
if [[ "$pattern" =~ find[[:space:]]+(/[^[:space:]]+) ]]; then
|
||||
dir_path="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Check if specific directory exists (for patterns that search specific dirs)
|
||||
if [[ -n "$dir_path" && "$dir_path" != "/dev" ]]; then
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
# Directory doesn't exist - return empty, not an error
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normal device discovery
|
||||
eval "$pattern" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get device information for a given device path
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
|
||||
if [[ ! -e "$device_path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ ! -c "$device_path" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local stat_output
|
||||
if ! stat_output=$(stat -c "%F:%t:%T:%f:%u:%g" "$device_path" 2>/dev/null); then
|
||||
return 1
|
||||
fi
|
||||
|
||||
local major minor file_mode uid gid
|
||||
IFS=':' read -r _ major minor file_mode uid gid <<<"$stat_output"
|
||||
|
||||
# Convert hex to decimal
|
||||
major=$((0x$major))
|
||||
minor=$((0x$minor))
|
||||
file_mode=$((0x$file_mode))
|
||||
|
||||
# Determine device type
|
||||
local device_type="c"
|
||||
|
||||
# Return colon-separated values
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
}
|
||||
|
||||
# Check if device directory exists
|
||||
should_process_device_type() {
|
||||
local device_type="$1"
|
||||
local directory="$2"
|
||||
|
||||
# Process if the directory exists and is accessible
|
||||
[[ -d "$directory" ]] 2>/dev/null
|
||||
}
|
||||
|
||||
# GPU device discovery for wayland-client-devices
|
||||
discover_gpu_devices() {
|
||||
# Normal device discovery - check if directory exists first
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
find /dev/dri -type c \( -regex ".*/render.*" \) 2>/dev/null || true
|
||||
else
|
||||
# No GPU devices directory - return empty (not an error)
|
||||
return 0
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
#!/bin/bash
|
||||
# Mock Device Support Library for OCI Hooks Testing
|
||||
# This file provides device mocking functionality for testing OCI hooks
|
||||
# without requiring actual system devices.
|
||||
|
||||
# Source common utilities
|
||||
# shellcheck source=./common.sh disable=SC1091
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
# Device discovery function with test override support
|
||||
discover_devices() {
|
||||
local pattern="$1"
|
||||
local device_type="$2"
|
||||
|
||||
# Check for test override environment variable
|
||||
local override_var="TEST_MOCK_${device_type^^}_DEVICES"
|
||||
local override_value="${!override_var:-}"
|
||||
|
||||
if [[ -n "$override_value" ]]; then
|
||||
# Use test-provided mock devices
|
||||
log "DEBUG" "Using mock $device_type devices: $override_value"
|
||||
# Convert comma-separated devices to null-terminated individual paths
|
||||
IFS=',' read -ra device_array <<<"$override_value"
|
||||
for device in "${device_array[@]}"; do
|
||||
printf '%s\0' "$device"
|
||||
done
|
||||
return
|
||||
fi
|
||||
|
||||
# Extract directory path from pattern to check existence first
|
||||
local dir_path=""
|
||||
if [[ "$pattern" =~ find[[:space:]]+(/[^[:space:]]+) ]]; then
|
||||
dir_path="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# Check if specific directory exists (for patterns that search specific dirs)
|
||||
if [[ -n "$dir_path" && "$dir_path" != "/dev" ]]; then
|
||||
if [[ ! -d "$dir_path" ]]; then
|
||||
# Directory doesn't exist - return empty, not an error
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Normal device discovery fallback
|
||||
eval "$pattern" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Get device information - works with real temporary device files created by mknod
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
|
||||
# First try to get real device info (works if mknod succeeded)
|
||||
if [[ -e "$device_path" && -c "$device_path" ]]; then
|
||||
local stat_output
|
||||
if stat_output=$(stat -c "%F:%t:%T:%f:%u:%g" "$device_path" 2>/dev/null); then
|
||||
local major minor file_mode uid gid
|
||||
IFS=':' read -r _ major minor file_mode uid gid <<<"$stat_output"
|
||||
|
||||
# Convert hex to decimal
|
||||
major=$((0x$major))
|
||||
minor=$((0x$minor))
|
||||
file_mode=$((0x$file_mode))
|
||||
|
||||
# Determine device type
|
||||
local device_type="c"
|
||||
|
||||
# Return colon-separated values
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# If real device doesn't exist and we're in test mode, generate mock device info
|
||||
if [[ "${FORCE_MOCK_DEVICES:-false}" == "true" || -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Check if this path matches any of our test mock device patterns
|
||||
local is_mock=false
|
||||
for env_var in TEST_MOCK_AUDIO_DEVICES TEST_MOCK_VIDEO_DEVICES TEST_MOCK_INPUT_DEVICES \
|
||||
TEST_MOCK_GPU_DEVICES TEST_MOCK_TTYUSB_DEVICES TEST_MOCK_DVB_DEVICES \
|
||||
TEST_MOCK_RADIO_DEVICES TEST_MOCK_TTYS_DEVICES; do
|
||||
if [[ -n "${!env_var:-}" ]]; then
|
||||
IFS=',' read -ra mock_devices <<<"${!env_var}"
|
||||
for mock_device in "${mock_devices[@]}"; do
|
||||
if [[ "$mock_device" == "$device_path" ]]; then
|
||||
is_mock=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [[ "$is_mock" == "true" ]]; then
|
||||
# Generate mock device info based on device path patterns
|
||||
local major minor file_mode=8624 uid=0 gid=0 # Default values
|
||||
local device_type="c"
|
||||
|
||||
case "$device_path" in
|
||||
*/snd/*)
|
||||
major=116
|
||||
minor=$((RANDOM % 20 + 1))
|
||||
;; # ALSA devices
|
||||
*/video*)
|
||||
major=81
|
||||
minor=$((RANDOM % 10))
|
||||
;; # Video devices
|
||||
*/input/*)
|
||||
major=13
|
||||
minor=$((RANDOM % 32))
|
||||
;; # Input devices
|
||||
*/dri/*)
|
||||
major=226
|
||||
minor=$((RANDOM % 10 + 128))
|
||||
;; # DRI/GPU devices
|
||||
*/ttyUSB*)
|
||||
major=188
|
||||
minor=$((RANDOM % 10))
|
||||
;; # USB serial
|
||||
*/dvb/*)
|
||||
major=212
|
||||
minor=$((RANDOM % 10))
|
||||
;; # DVB devices
|
||||
*/radio*)
|
||||
major=81
|
||||
minor=$((RANDOM % 10 + 64))
|
||||
;; # Radio devices
|
||||
*/tty[0-9]*)
|
||||
major=4
|
||||
minor=$((RANDOM % 10))
|
||||
;; # TTY devices
|
||||
*)
|
||||
major=1
|
||||
minor=$((RANDOM % 10))
|
||||
;; # Fallback
|
||||
esac
|
||||
|
||||
# Adjust gid for audio devices (usually audio group = 63)
|
||||
if [[ "$device_path" == */snd/* ]]; then
|
||||
gid=63
|
||||
fi
|
||||
|
||||
echo "$device_type:$major:$minor:$file_mode:$uid:$gid"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Neither real device nor mock - fail
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if device directory exists or we have mock devices for the type
|
||||
should_process_device_type() {
|
||||
local device_type="$1"
|
||||
local directory="$2"
|
||||
|
||||
local override_var="TEST_MOCK_${device_type^^}_DEVICES"
|
||||
local override_value="${!override_var:-}"
|
||||
|
||||
# Process if we have mock devices OR the directory exists and is accessible
|
||||
[[ -n "$override_value" ]] || [[ -d "$directory" ]] 2>/dev/null
|
||||
}
|
||||
|
||||
# GPU device discovery with mocking support for wayland-client-devices
|
||||
discover_gpu_devices() {
|
||||
if [[ -n "${TEST_MOCK_GPU_DEVICES:-}" ]]; then
|
||||
# Use test-provided mock devices
|
||||
log "DEBUG" "Using mock GPU devices: $TEST_MOCK_GPU_DEVICES"
|
||||
echo "$TEST_MOCK_GPU_DEVICES" | tr ',' ' '
|
||||
else
|
||||
# Normal device discovery - check if directory exists first
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
find /dev/dri -type c \( -regex ".*/render.*" \) 2>/dev/null || true
|
||||
else
|
||||
# No GPU devices directory - return empty (not an error)
|
||||
log "DEBUG" "No /dev/dri directory found - no GPU devices available"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
}
|
|
@ -0,0 +1,281 @@
|
|||
# QM Device Manager OCI Hook
|
||||
|
||||
The QM Device Manager OCI Hook provides dynamic device access management for QM containers. This lightweight shell script hook replaces static drop-in configurations with a flexible, annotation-based approach to device mounting.
|
||||
|
||||
## Overview
|
||||
|
||||
The device manager hook allows containers to request specific device access through annotations. The hook dynamically discovers and mounts the requested devices at container creation time. It supports both device categories and Wayland seat-based device access.
|
||||
|
||||
## How Device Injection Works
|
||||
|
||||
The QM Device Manager hook operates during the OCI container **precreate** phase, intercepting container creation to dynamically inject device access. When QM starts a container with device annotations, the hook:
|
||||
|
||||
1. Discovers available devices on the host system based on annotation patterns
|
||||
2. Validates device accessibility and permissions
|
||||
3. Injects device access into the container's OCI specification via `linux.resources.devices[]`
|
||||
4. Adds device mounts to make devices available inside the container
|
||||
|
||||
### OCI Device Injection Format
|
||||
|
||||
The hook modifies the container's OCI runtime specification to include device access rules:
|
||||
|
||||
```json
|
||||
{
|
||||
"linux": {
|
||||
"resources": {
|
||||
"devices": [
|
||||
{
|
||||
"allow": true,
|
||||
"type": "c",
|
||||
"major": 116,
|
||||
"minor": 0,
|
||||
"access": "rwm"
|
||||
},
|
||||
{
|
||||
"allow": true,
|
||||
"type": "c",
|
||||
"major": 116,
|
||||
"minor": 1,
|
||||
"access": "rwm"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
{
|
||||
"source": "/dev/snd/controlC0",
|
||||
"destination": "/dev/snd/controlC0",
|
||||
"type": "bind",
|
||||
"options": ["bind", "rprivate"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This ensures the container has both cgroup device access permissions and the actual device nodes mounted.
|
||||
|
||||
### Testing and Verification
|
||||
|
||||
The device injection process is comprehensively tested in the test suite:
|
||||
|
||||
- Device Discovery Tests (`test_qm_devices.py`): Verifies correct device discovery for all supported types
|
||||
- Annotation Processing (`test_qm_validation.py`): Tests annotation parsing and validation
|
||||
- Mock Device Support (`test_utils.py`): Provides mock device infrastructure for CI testing
|
||||
- Integration Tests (`test_qm_performance.py`): End-to-end hook execution testing
|
||||
|
||||
Run tests to verify device injection:
|
||||
|
||||
```bash
|
||||
cd oci-hooks && tox -e unit -- -k "device"
|
||||
```
|
||||
|
||||
### Library Architecture
|
||||
|
||||
The QM Device Manager hook uses a modular library architecture for maintainability and testing:
|
||||
|
||||
#### Core Libraries
|
||||
|
||||
- `common.sh`: Shared logging and utility functions used across all OCI hooks
|
||||
- `device-support.sh`: Production device discovery and management functions
|
||||
- `mock-device-support.sh`: Testing-specific device simulation functions
|
||||
|
||||
#### Mock Device Support for Testing
|
||||
|
||||
The `mock-device-support.sh` library enables comprehensive testing without requiring actual hardware devices. It works by:
|
||||
|
||||
Environment Variable Control - Tests set environment variables like:
|
||||
|
||||
```bash
|
||||
TEST_MOCK_AUDIO_DEVICES="/tmp/test_devices/snd/controlC0,/tmp/test_devices/snd/pcmC0D0p"
|
||||
TEST_MOCK_VIDEO_DEVICES="/tmp/test_devices/video0,/tmp/test_devices/video1"
|
||||
TEST_LOGFILE="/tmp/test.log" # Enables test mode
|
||||
```
|
||||
|
||||
Mock Device Creation - Python test framework creates temporary device files:
|
||||
|
||||
```python
|
||||
# In test_utils.py DeviceDetector class
|
||||
device_path = Path("/tmp/test_devices/snd/controlC0")
|
||||
device_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
device_path.touch() # Creates regular file as mock device
|
||||
```
|
||||
|
||||
Hook Library Selection - The hook script automatically detects test mode and sources the appropriate library:
|
||||
|
||||
```bash
|
||||
# In oci-qm-device-manager script
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
source /usr/libexec/oci/lib/mock-device-support.sh
|
||||
else
|
||||
source /usr/libexec/oci/lib/device-support.sh
|
||||
fi
|
||||
```
|
||||
|
||||
**NOTE**: mock device library is not supported in a productive environment.
|
||||
|
||||
Mock Functions - `mock-device-support.sh` overrides discovery functions to return test devices:
|
||||
|
||||
```bash
|
||||
discover_audio_devices() {
|
||||
if [[ -n "${TEST_MOCK_AUDIO_DEVICES:-}" ]]; then
|
||||
echo "${TEST_MOCK_AUDIO_DEVICES}" | tr ',' '\n'
|
||||
fi
|
||||
}
|
||||
|
||||
get_device_info() {
|
||||
local device_path="$1"
|
||||
# Returns mock device info for regular files in test mode
|
||||
echo "c 116:0" # Mock major:minor for audio devices
|
||||
}
|
||||
```
|
||||
|
||||
This architecture allows the same hook script to work with both real hardware devices in production and simulated devices in CI testing environments.
|
||||
|
||||
## Supported Device Types
|
||||
|
||||
### Traditional Device Categories
|
||||
|
||||
| Device Type | Annotation | Devices Mounted | Description |
|
||||
|-------------|------------|-----------------|-------------|
|
||||
| audio | `org.containers.qm.device.audio=true` | `/dev/snd/*` | ALSA sound devices |
|
||||
| video | `org.containers.qm.device.video=true` | `/dev/video*`, `/dev/media*` | V4L2 video devices |
|
||||
| input | `org.containers.qm.device.input=true` | `/dev/input/*` | Input devices (keyboard, mouse, etc.) |
|
||||
| ttys | `org.containers.qm.device.ttys=true` | `/dev/tty0-7` | Virtual TTY devices for window managers |
|
||||
| ttyUSB | `org.containers.qm.device.ttyUSB=true` | `/dev/ttyUSB*` | USB TTY devices for serial communication |
|
||||
| dvb | `org.containers.qm.device.dvb=true` | `/dev/dvb/*` | DVB digital TV devices |
|
||||
| radio | `org.containers.qm.device.radio=true` | `/dev/radio*` | Radio devices |
|
||||
|
||||
### Wayland Seat Support
|
||||
|
||||
| Annotation | Purpose | Description |
|
||||
|------------|---------|-------------|
|
||||
| `org.containers.qm.wayland.seat=<seat_name>` | Multi-seat support | Mounts devices associated with a specific Wayland seat (e.g., `seat0`) |
|
||||
|
||||
The Wayland seat functionality dynamically discovers and mounts devices associated with the specified seat using `loginctl seat-status`. This enables proper multi-seat support for Wayland environments where different users may be logged into different seats.
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Systemd Drop-In Files
|
||||
|
||||
1. Create a drop-in directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/containers/systemd/qm.container.d/
|
||||
```
|
||||
|
||||
2. Create a dropin file (e.g., `devices.conf`):
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Annotation=org.containers.qm.device.audio=true
|
||||
Annotation=org.containers.qm.device.ttys=true
|
||||
# Wayland seat support
|
||||
Annotation=org.containers.qm.wayland.seat=seat0
|
||||
```
|
||||
|
||||
### Generated Systemd Service
|
||||
|
||||
When using Quadlet (`.container` files), the systemd generator creates the actual `ExecStart` commands that execute `podman run`. You can preview these with:
|
||||
|
||||
```bash
|
||||
# View generated systemd service commands
|
||||
/usr/lib/systemd/system-generators/podman-system-generator --dryrun
|
||||
|
||||
# Check generated service files
|
||||
ls -la /var/lib/systemd/generated/
|
||||
cat /var/lib/systemd/generated/container-name.service
|
||||
```
|
||||
|
||||
#### Example Generated Commands within QM
|
||||
|
||||
For a Quadlet container with device annotations:
|
||||
|
||||
Quadlet File (`/etc/containers/systemd/audio-app.container`):
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Audio Application Container
|
||||
After=local-fs.target
|
||||
|
||||
[Container]
|
||||
Image=my-audio-app:latest
|
||||
Annotation=org.containers.qm.device.audio=true
|
||||
Annotation=org.containers.qm.device.video=true
|
||||
Exec=sleep infinity
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
```
|
||||
|
||||
Generated ExecStart Command:
|
||||
|
||||
```bash
|
||||
ExecStart=/usr/bin/podman run \
|
||||
--cidfile=%t/%N.cid \
|
||||
--cgroups=split \
|
||||
--replace \
|
||||
--rm -d \
|
||||
--sdnotify=container \
|
||||
--name=audio-app \
|
||||
--annotation=org.containers.qm.device.audio=true \
|
||||
--annotation=org.containers.qm.device.video=true \
|
||||
localhost/qm/my-audio-app:latest sleep infinity
|
||||
```
|
||||
|
||||
When this service starts, the `qm-device-manager` hook intercepts the container creation and dynamically adds device mounts like:
|
||||
|
||||
- `--device=/dev/snd/controlC0:/dev/snd/controlC0`
|
||||
- `--device=/dev/snd/pcmC0D0p:/dev/snd/pcmC0D0p`
|
||||
- `--device=/dev/video0:/dev/video0`
|
||||
|
||||
## Logging
|
||||
|
||||
The hook logs activity to `/var/log/qm-device-manager.log` for debugging and monitoring:
|
||||
|
||||
```bash
|
||||
# View hook activity
|
||||
tail -f /var/log/qm-device-manager.log
|
||||
|
||||
# Check device discovery for a specific container
|
||||
grep "audio" /var/log/qm-device-manager.log
|
||||
|
||||
# Check Wayland seat processing
|
||||
grep "Wayland seat" /var/log/qm-device-manager.log
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Only devices that exist on the host are mounted
|
||||
- Device permissions are preserved from the host
|
||||
- Hook validates device accessibility before mounting
|
||||
- Annotation-based activation prevents accidental device exposure
|
||||
- Wayland seat integration respects systemd-logind seat assignments
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Device Not Available
|
||||
|
||||
If a requested device is not mounted:
|
||||
|
||||
1. Check if the device exists on the host: `ls -la /dev/snd/`
|
||||
2. Verify the annotation syntax: `org.containers.qm.device.audio=true`
|
||||
3. Check hook logs: `grep ERROR /var/log/qm-device-manager.log`
|
||||
4. Ensure the device is accessible: `test -c /dev/snd/controlC0`
|
||||
|
||||
### Wayland Seat Issues
|
||||
|
||||
If Wayland seat devices are not mounted:
|
||||
|
||||
1. Check seat status: `loginctl seat-status seat0`
|
||||
2. Verify seat annotation: `org.containers.qm.wayland.seat=seat0`
|
||||
3. Check systemd-logind service: `systemctl status systemd-logind`
|
||||
4. Review seat logs: `grep "seat0" /var/log/qm-device-manager.log`
|
||||
|
||||
### Hook Not Triggering
|
||||
|
||||
If the hook is not being called:
|
||||
|
||||
1. Verify hook installation: `ls -la /usr/libexec/oci/hooks.d/`
|
||||
2. Check hook configuration: `cat /usr/share/containers/oci/hooks.d/oci-qm-device-manager.json`
|
||||
3. Validate annotation pattern matching
|
||||
4. Check podman/crun OCI hook support
|
|
@ -0,0 +1,391 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# QM Device Manager OCI Hook
|
||||
#
|
||||
# This hook dynamically manages device access for QM containers based on annotations.
|
||||
# It replaces the static drop-in configurations from individual subpackages with
|
||||
# dynamic device mounting based on container annotations.
|
||||
#
|
||||
# Supported devices:
|
||||
# - audio: /dev/snd/* (ALSA sound devices)
|
||||
# - video: /dev/video*, /dev/media* (V4L2 video devices)
|
||||
# - input: /dev/input/* (input devices like keyboards, mice)
|
||||
# - ttys: /dev/tty0-7 (virtual TTY devices for window managers)
|
||||
# - ttyUSB: /dev/ttyUSB* (USB TTY devices for serial communication)
|
||||
# - dvb: /dev/dvb/* (DVB digital TV devices)
|
||||
# - radio: /dev/radio* (radio devices)
|
||||
#
|
||||
# Supported device annotations:
|
||||
# - org.containers.qm.device.audio=true # /dev/snd/* (ALSA sound devices)
|
||||
# - org.containers.qm.device.video=true # /dev/video*, /dev/media* (V4L2 video devices)
|
||||
# - org.containers.qm.device.input=true # /dev/input/* (input devices)
|
||||
# - org.containers.qm.device.ttys=true # /dev/tty0-7 (virtual TTY devices)
|
||||
# - org.containers.qm.device.ttyUSB=true # /dev/ttyUSB* (USB TTY devices)
|
||||
# - org.containers.qm.device.dvb=true # /dev/dvb/* (DVB digital TV devices)
|
||||
# - org.containers.qm.device.radio=true # /dev/radio* (radio devices)
|
||||
#
|
||||
# Supported Wayland annotations:
|
||||
# - org.containers.qm.wayland.seat=<seat_name> # Devices for specific systemd-logind seat
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source common utilities and appropriate device support library
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# shellcheck source=../lib/common.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Test mode - use mock device support
|
||||
# shellcheck source=../lib/mock-device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/mock-device-support.sh"
|
||||
else
|
||||
# Normal mode - use standard device support
|
||||
# shellcheck source=../lib/device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/device-support.sh"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
LOGFILE="${TEST_LOGFILE:-/var/log/qm-device-manager.log}"
|
||||
# shellcheck disable=SC2034 # Used by log() function in device-support.sh
|
||||
HOOK_NAME="qm-device-manager"
|
||||
|
||||
# Process input devices with optional filtering and different output modes
|
||||
process_input_devices() {
|
||||
local mode="$1" # "direct" or "collect"
|
||||
local spec_json="$2" # current spec (for direct mode)
|
||||
local devname_list_var="$3" # variable name for device list (for collect mode)
|
||||
local filter_pattern="${4:-}" # optional regex filter
|
||||
local log_prefix="${5:-input}" # log message prefix
|
||||
|
||||
local device_count=0
|
||||
|
||||
if [[ ! -d "/dev/input" && -z "${TEST_LOGFILE:-}" ]]; then
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
log "INFO" "Added $device_count $log_prefix devices (no /dev/input directory)"
|
||||
echo "$spec_json"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "collect" ]]; then
|
||||
# For collect mode, we need to use nameref to modify the array
|
||||
local -n devname_list_ref="$devname_list_var"
|
||||
fi
|
||||
|
||||
while IFS= read -r -d '' device_path; do
|
||||
# Apply filter if provided
|
||||
if [[ -n "$filter_pattern" ]] && [[ ! "$device_path" =~ $filter_pattern ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
# Add device directly to spec
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
elif [[ "$mode" == "collect" ]]; then
|
||||
# Add device to collection list
|
||||
devname_list_ref+=("$device_path")
|
||||
log "INFO" "Adding $log_prefix device: $device_path"
|
||||
((device_count++))
|
||||
fi
|
||||
done < <(discover_devices "find /dev/input -type c -print0" "input")
|
||||
|
||||
if [[ "$mode" == "direct" ]]; then
|
||||
log "INFO" "Added $device_count $log_prefix devices"
|
||||
echo "$spec_json"
|
||||
fi
|
||||
}
|
||||
|
||||
# Add device to OCI spec using jq
|
||||
add_device_to_spec() {
|
||||
local spec_json="$1"
|
||||
local device_path="$2"
|
||||
local add_resources="${3:-false}"
|
||||
local device_info major minor file_mode uid gid device_type
|
||||
|
||||
if ! device_info=$(get_device_info "$device_path"); then
|
||||
log "WARNING" "Failed to get device info for $device_path"
|
||||
echo "$spec_json"
|
||||
return
|
||||
fi
|
||||
|
||||
IFS=':' read -r device_type major minor file_mode uid gid <<<"$device_info"
|
||||
log "INFO" "Adding device: $device_path (type=$device_type, major=$major, minor=$minor)"
|
||||
|
||||
# Ensure .linux.devices array exists
|
||||
local temp_spec
|
||||
if ! temp_spec=$(echo "$spec_json" | jq --compact-output 'if .linux.devices == null then .linux.devices = [] else . end' 2>/dev/null); then
|
||||
log "ERROR" "Failed to ensure .linux.devices array exists for $device_path"
|
||||
echo "$spec_json"
|
||||
return
|
||||
fi
|
||||
|
||||
# Add device if it doesn't already exist
|
||||
local result
|
||||
if ! result=$(echo "$temp_spec" | jq --compact-output \
|
||||
--arg path "$device_path" \
|
||||
--arg type "$device_type" \
|
||||
--argjson major "$major" \
|
||||
--argjson minor "$minor" \
|
||||
--argjson fileMode "$file_mode" \
|
||||
--argjson uid "$uid" \
|
||||
--argjson gid "$gid" \
|
||||
'if (.linux.devices | map(.path) | index($path)) == null then .linux.devices += [{"path": $path, "type": $type, "major": $major, "minor": $minor, "fileMode": $fileMode, "uid": $uid, "gid": $gid}] else . end' 2>/dev/null); then
|
||||
log "ERROR" "Failed to add device $device_path to spec"
|
||||
echo "$temp_spec" # Return the spec with array at least initialized
|
||||
return
|
||||
fi
|
||||
|
||||
# Add device resources if requested (for Wayland devices)
|
||||
if [[ "$add_resources" == "true" ]]; then
|
||||
if ! result=$(echo "$result" | jq --compact-output \
|
||||
--arg type "$device_type" \
|
||||
--argjson major "$major" \
|
||||
--argjson minor "$minor" \
|
||||
'if .linux.resources == null then .linux.resources = {} else . end |
|
||||
if .linux.resources.devices == null then .linux.resources.devices = [] else . end |
|
||||
if (.linux.resources.devices | map(select(.type == $type and .major == $major and .minor == $minor)) | length) == 0 then
|
||||
.linux.resources.devices += [{"allow": true, "type": $type, "major": $major, "minor": $minor, "access": "rwm"}]
|
||||
else . end' 2>/dev/null); then
|
||||
log "WARNING" "Failed to add device resources for $device_path, continuing with device only"
|
||||
else
|
||||
log "INFO" "Added device resources for: $device_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "$result"
|
||||
}
|
||||
|
||||
# Process device annotations (org.containers.qm.device.*)
|
||||
process_device_annotation() {
|
||||
local spec_json="$1"
|
||||
local device_type="$2"
|
||||
|
||||
log "INFO" "Processing device type: $device_type"
|
||||
|
||||
case "$device_type" in
|
||||
"audio")
|
||||
# ALSA sound devices
|
||||
local device_count=0
|
||||
if should_process_device_type "audio" "/dev/snd"; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev/snd -type c -print0" "audio")
|
||||
fi
|
||||
log "INFO" "Added $device_count audio devices"
|
||||
;;
|
||||
"video")
|
||||
# V4L2 video devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 \\( -name \"video*\" -o -name \"media*\" \\) -type c -print0" "video")
|
||||
log "INFO" "Added $device_count video devices"
|
||||
;;
|
||||
"input")
|
||||
# Input devices
|
||||
spec_json=$(process_input_devices "direct" "$spec_json" "" "" "input")
|
||||
;;
|
||||
"ttys")
|
||||
# Virtual TTY devices (tty0-7)
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name 'tty[0-7]' -type c -print0" "ttys")
|
||||
log "INFO" "Added $device_count TTY devices"
|
||||
;;
|
||||
"ttyUSB")
|
||||
# USB TTY devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name \"ttyUSB*\" -type c -print0" "ttyUSB")
|
||||
log "INFO" "Added $device_count USB TTY devices"
|
||||
;;
|
||||
"dvb")
|
||||
# DVB digital TV devices
|
||||
local device_count=0
|
||||
if should_process_device_type "dvb" "/dev/dvb"; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev/dvb -type c -print0" "dvb")
|
||||
fi
|
||||
log "INFO" "Added $device_count DVB devices"
|
||||
;;
|
||||
"radio")
|
||||
# Radio devices
|
||||
local device_count=0
|
||||
while IFS= read -r -d '' device_path; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path")
|
||||
((device_count++))
|
||||
done < <(discover_devices "find /dev -maxdepth 1 -name \"radio*\" -type c -print0" "radio")
|
||||
log "INFO" "Added $device_count radio devices"
|
||||
;;
|
||||
*)
|
||||
log "WARNING" "Unknown device type: $device_type"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "$spec_json"
|
||||
}
|
||||
|
||||
# Process Wayland seat annotation (org.containers.qm.wayland.seat)
|
||||
process_wayland_seat() {
|
||||
local spec_json="$1"
|
||||
local seat_name="$2"
|
||||
|
||||
log "INFO" "Processing Wayland seat: $seat_name"
|
||||
|
||||
local device_count=0
|
||||
local devname_list=()
|
||||
|
||||
# Get devices associated with the systemd-logind seat
|
||||
if command -v loginctl >/dev/null 2>&1; then
|
||||
local seat_devices
|
||||
if seat_devices=$(loginctl seat-status "$seat_name" 2>/dev/null | grep -oP '/sys\S+'); then
|
||||
log "INFO" "Found seat system devices for $seat_name"
|
||||
|
||||
while IFS= read -r device; do
|
||||
if [[ -n "$device" ]]; then
|
||||
local devname
|
||||
if devname=$(udevadm info -x "$device" 2>/dev/null | grep -oP '^E: DEVNAME=\K.*'); then
|
||||
if [[ -n "$devname" && -e "$devname" ]]; then
|
||||
devname_list+=("$devname")
|
||||
log "INFO" "Found seat device: $devname"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done <<<"$seat_devices"
|
||||
else
|
||||
log "WARNING" "No devices found for seat $seat_name or seat does not exist"
|
||||
fi
|
||||
else
|
||||
log "WARNING" "loginctl not available, cannot query seat devices"
|
||||
fi
|
||||
|
||||
# Add common input devices
|
||||
process_input_devices "collect" "" "devname_list" "/dev/input/(event[0-9]+|mice[0-9]*|mouse[0-9]+)$" "input"
|
||||
|
||||
# Add GPU render devices
|
||||
if [[ -d "/dev/dri" ]]; then
|
||||
while IFS= read -r -d '' device_path; do
|
||||
if [[ "$device_path" =~ /dev/dri/render.* ]]; then
|
||||
devname_list+=("$device_path")
|
||||
log "INFO" "Adding render device: $device_path"
|
||||
fi
|
||||
done < <(discover_devices "find /dev/dri -type c -name \"render*\" -print0" "gpu")
|
||||
fi
|
||||
|
||||
# Add all devices to spec with resources
|
||||
for device_path in "${devname_list[@]}"; do
|
||||
spec_json=$(add_device_to_spec "$spec_json" "$device_path" "true")
|
||||
((device_count++))
|
||||
done
|
||||
|
||||
log "INFO" "Added $device_count Wayland seat devices for $seat_name"
|
||||
echo "$spec_json"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
local spec_json
|
||||
local annotations
|
||||
local total_devices=0
|
||||
|
||||
# Read OCI spec from stdin
|
||||
if ! spec_json=$(cat); then
|
||||
log "ERROR" "Failed to read OCI spec from stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ensure linux section exists
|
||||
if ! echo "$spec_json" | jq -e '.linux' >/dev/null 2>&1; then
|
||||
spec_json=$(echo "$spec_json" | jq '.linux = {}')
|
||||
fi
|
||||
|
||||
# Get all QM-related annotations
|
||||
annotations=$(echo "$spec_json" | jq -r '.annotations // {} | to_entries[] | select(.key | startswith("org.containers.qm.")) | "\(.key)=\(.value)"' 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$annotations" ]]; then
|
||||
log "INFO" "No QM device annotations found"
|
||||
echo "$spec_json"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "INFO" "Processing QM device annotations"
|
||||
|
||||
# Process each annotation
|
||||
while IFS= read -r annotation; do
|
||||
if [[ -z "$annotation" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract annotation key and value
|
||||
annotation_key="${annotation%%=*}"
|
||||
annotation_value="${annotation#*=}"
|
||||
|
||||
log "INFO" "Processing annotation: $annotation"
|
||||
|
||||
case "$annotation_key" in
|
||||
"org.containers.qm.device."*)
|
||||
# Traditional device annotation
|
||||
device_type="${annotation_key#org.containers.qm.device.}"
|
||||
|
||||
# Skip if value is not true/1/yes
|
||||
if [[ ! "$annotation_value" =~ ^(true|1|yes)$ ]]; then
|
||||
log "INFO" "Skipping device annotation with invalid value: $annotation"
|
||||
continue
|
||||
fi
|
||||
|
||||
spec_json=$(process_device_annotation "$spec_json" "$device_type" "$annotation_value")
|
||||
;;
|
||||
"org.containers.qm.wayland.seat")
|
||||
# Wayland seat annotation
|
||||
if [[ -n "$annotation_value" && "$annotation_value" != "null" ]]; then
|
||||
spec_json=$(process_wayland_seat "$spec_json" "$annotation_value")
|
||||
else
|
||||
log "INFO" "Skipping Wayland seat annotation with empty value"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
log "INFO" "Skipping unknown QM annotation: $annotation_key"
|
||||
;;
|
||||
esac
|
||||
|
||||
done <<<"$annotations"
|
||||
|
||||
# Count total devices added
|
||||
total_devices=$(echo "$spec_json" | jq '.linux.devices // [] | length' 2>/dev/null || echo "0")
|
||||
log "INFO" "Total devices in spec: $total_devices"
|
||||
|
||||
# Output the modified spec
|
||||
echo "$spec_json"
|
||||
|
||||
log "INFO" "QM Device Manager hook completed successfully"
|
||||
}
|
||||
|
||||
# Ensure log file exists
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
touch "$LOGFILE"
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"hook": {
|
||||
"path": "/usr/libexec/oci/hooks.d/oci-qm-device-manager"
|
||||
},
|
||||
"when": {
|
||||
"annotations": {
|
||||
"^org\\.containers\\.qm\\.device\\..*$": "^.*$",
|
||||
"^org\\.containers\\.qm\\.wayland\\.seat": "^.*$"
|
||||
}
|
||||
},
|
||||
"stages": ["precreate"]
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
.venv/
|
||||
.env/
|
||||
|
||||
# Tox environments and cache
|
||||
.tox/
|
||||
.tox-*/
|
||||
|
||||
# Test reports and artifacts
|
||||
reports/
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.nyc_output/
|
||||
|
||||
# Python cache and bytecode
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# IDEs and editors
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
/tmp/
|
||||
.tmp/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
|
@ -0,0 +1,50 @@
|
|||
[MAIN]
|
||||
# Specify a score threshold to be exceeded before program exits with error
|
||||
fail-under=10.0
|
||||
|
||||
# Use multiple processes to speed up Pylint
|
||||
jobs=1
|
||||
|
||||
# Allow loading of arbitrary C extensions
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# Disable specific warnings that are common/acceptable in test files
|
||||
disable=
|
||||
# Import related (pytest/test dependencies are OK)
|
||||
import-error,
|
||||
# Test-specific patterns
|
||||
unused-argument, # Fixtures often have unused parameters
|
||||
redefined-outer-name, # Fixture names often match test parameters
|
||||
unused-variable, # Test outputs not always used
|
||||
inconsistent-return-statements, # pytest.fail() is intentional pattern
|
||||
# Less critical for tests
|
||||
too-few-public-methods,
|
||||
too-many-arguments,
|
||||
too-many-locals,
|
||||
duplicate-code, # Some duplication in tests is acceptable
|
||||
# File organization
|
||||
wrong-import-order, # Less critical in test files
|
||||
# Naming
|
||||
invalid-name, # Test method names can be long and descriptive
|
||||
|
||||
[DESIGN]
|
||||
# Maximum number of arguments for function / method
|
||||
max-args=8
|
||||
|
||||
# Maximum number of locals for function / method body
|
||||
max-locals=20
|
||||
|
||||
[SIMILARITIES]
|
||||
# Minimum lines number of a similarity
|
||||
min-similarity-lines=10
|
||||
|
||||
[BASIC]
|
||||
# Naming style matching correct function names (relaxed for tests)
|
||||
function-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Naming style matching correct method names (relaxed for tests)
|
||||
method-rgx=[a-z_][a-z0-9_]{2,50}$
|
||||
|
||||
# Naming style matching correct class names
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
|
@ -0,0 +1,107 @@
|
|||
# OCI Hooks Unit Tests
|
||||
|
||||
Simple unit tests for QM OCI hooks using pytest and tox.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Set up virtual environment
|
||||
cd oci-hooks
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Install tox
|
||||
pip install tox
|
||||
|
||||
# Run unit tests
|
||||
tox -e unit
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
### Test Files
|
||||
|
||||
- `test_qm_device_manager.py` - Tests for the unified device manager hook
|
||||
- `test_wayland_client_devices.py` - Tests for the Wayland client GPU hook
|
||||
|
||||
### Test Categories
|
||||
|
||||
- **Unit tests**: Fast tests with no external dependencies
|
||||
- **Integration tests**: Tests that may require system resources
|
||||
- **Performance tests**: Tests with timing requirements
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Using Tox
|
||||
|
||||
```bash
|
||||
tox -e all # Run all tests
|
||||
tox -e lint # Check code formatting for python and shell
|
||||
tox -e format # Auto-format code with black
|
||||
```
|
||||
|
||||
### Direct Pytest
|
||||
|
||||
If you prefer to avoid tox, you can run pytest directly:
|
||||
|
||||
```bash
|
||||
# Set up virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # Linux
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Run tests
|
||||
pytest -m unit --tb=short -v # Unit tests only
|
||||
pytest test_qm_device_manager.py # Specific test file
|
||||
```
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
Tests run automatically in GitHub Actions on:
|
||||
|
||||
- Push to `oci-hooks/**` paths
|
||||
- Pull requests affecting `oci-hooks/**` paths
|
||||
|
||||
Matrix testing across Python versions 3.9-3.12.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Make changes** to OCI hook scripts or tests
|
||||
2. **Run tests locally**: `tox -e unit`
|
||||
3. **Check code quality**: `tox -e lint`
|
||||
4. **Auto-format if needed**: `tox -e format`
|
||||
5. **Fix any failures** before committing
|
||||
6. **Push changes** - CI will run full test suite
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Permission Issues
|
||||
|
||||
If hooks fail with permission errors, ensure scripts are executable:
|
||||
|
||||
```bash
|
||||
chmod +x ../qm-device-manager/oci-qm-device-manager
|
||||
chmod +x ../wayland-client-devices/oci-qm-wayland-client-devices
|
||||
```
|
||||
|
||||
### Missing Dependencies
|
||||
|
||||
```bash
|
||||
# For tox
|
||||
pip install tox
|
||||
|
||||
# For direct pytest
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Test Environment
|
||||
|
||||
Tests use temporary log files to avoid permission issues:
|
||||
|
||||
- Set `TEST_LOGFILE` environment variable for custom log location
|
||||
- Default: `/tmp/test-oci-hooks.log` for test runs
|
|
@ -0,0 +1,98 @@
|
|||
"""Pytest configuration and fixtures for OCI hooks testing."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from device_utils import DeviceCounter, TempDevices
|
||||
from test_utils import (
|
||||
HookRunner,
|
||||
OciSpecValidator,
|
||||
LogChecker,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def test_env():
|
||||
"""Set up test environment with temporary log files."""
|
||||
with tempfile.TemporaryDirectory(prefix="oci_hooks_test_") as temp_dir:
|
||||
env = os.environ.copy()
|
||||
env["TEST_LOGFILE"] = os.path.join(temp_dir, "test-hook.log")
|
||||
yield {
|
||||
"env": env,
|
||||
"log_file": env["TEST_LOGFILE"],
|
||||
"temp_dir": temp_dir,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hook_runner(test_env):
|
||||
"""Create a HookRunner instance with test environment and hook paths."""
|
||||
yield HookRunner(test_env["env"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def oci_spec_validator():
|
||||
"""Provide validator for OCI specification JSON."""
|
||||
return OciSpecValidator.validate
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_specs():
|
||||
"""Sample OCI specifications for testing."""
|
||||
yield {
|
||||
"audio_device": {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
"multiple_devices": {
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true",
|
||||
"org.containers.qm.device.input": "true",
|
||||
"org.containers.qm.device.ttys": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
"wayland_seat": {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "seat0"},
|
||||
"linux": {},
|
||||
},
|
||||
"wayland_gpu": {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
"combined": {
|
||||
"annotations": {
|
||||
"org.containers.qm.device.audio": "true",
|
||||
"org.containers.qm.wayland.seat": "seat0",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
"invalid_device": {
|
||||
"annotations": {"org.containers.qm.device.audio": "false"},
|
||||
"linux": {},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def log_checker(test_env):
|
||||
"""Check hook log files for expected content."""
|
||||
return LogChecker(test_env["log_file"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_counter():
|
||||
"""Fixture providing device counting utilities."""
|
||||
return DeviceCounter
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_devices():
|
||||
"""Fixture providing on-demand temporary device creation."""
|
||||
with tempfile.TemporaryDirectory(prefix="test_devices_") as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
yield TempDevices(temp_path)
|
|
@ -0,0 +1,223 @@
|
|||
"""Device testing utilities for OCI hooks."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
|
||||
class DeviceCounter:
|
||||
"""Helper class for counting devices in OCI specifications."""
|
||||
|
||||
@staticmethod
|
||||
def count_devices(
|
||||
spec: Dict[str, Any], device_pattern: Optional[str] = None
|
||||
) -> int:
|
||||
"""Count devices in OCI spec, optionally filtered by path pattern."""
|
||||
devices = spec.get("linux", {}).get("devices", [])
|
||||
|
||||
if device_pattern is None:
|
||||
return len(devices)
|
||||
|
||||
return len([d for d in devices if device_pattern in d.get("path", "")])
|
||||
|
||||
@staticmethod
|
||||
def count_resources(spec: Dict[str, Any]) -> int:
|
||||
"""Count device resources in OCI spec."""
|
||||
resources = (
|
||||
spec.get("linux", {}).get("resources", {}).get("devices", [])
|
||||
)
|
||||
return len(resources)
|
||||
|
||||
@staticmethod
|
||||
def has_device_path(spec: Dict[str, Any], path: str) -> bool:
|
||||
"""Check if OCI spec contains a device with specific path."""
|
||||
devices = spec.get("linux", {}).get("devices", [])
|
||||
return any(d.get("path") == path for d in devices)
|
||||
|
||||
|
||||
@dataclass
|
||||
class DeviceDetector:
|
||||
"""Class for detecting real devices for testing."""
|
||||
|
||||
paths: List[str] = field(default_factory=list)
|
||||
expected_count: int = 0
|
||||
|
||||
def mock_devices(self, device_type) -> Optional[Dict[str, str]]:
|
||||
"""Mock devices for testing."""
|
||||
return {device_type: ",".join(self.paths)}
|
||||
|
||||
|
||||
class TempDevices:
|
||||
"""Class for creating temporary device files on-demand for testing."""
|
||||
|
||||
def __init__(self, temp_path: Path):
|
||||
"""Initialize TempDevices with temporary path."""
|
||||
self.temp_path = temp_path
|
||||
self._created_devices = {}
|
||||
|
||||
# Create base directory structure
|
||||
for subdir in ["snd", "dri", "input", "dvb"]:
|
||||
(self.temp_path / subdir).mkdir(exist_ok=True)
|
||||
|
||||
def _create_device(
|
||||
self, device_path: Path, major: int, minor: int
|
||||
) -> Path:
|
||||
"""Create device node with mknod if possible, fall back to env vars."""
|
||||
device_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if os.environ.get("FORCE_MOCK_DEVICES", "false").lower() != "true":
|
||||
try:
|
||||
# Try to create real device nodes when not forced
|
||||
os.mknod(
|
||||
device_path, stat.S_IFCHR | 0o600, os.makedev(major, minor)
|
||||
)
|
||||
except OSError:
|
||||
# Fallback to regular files if mknod fails
|
||||
device_path.touch()
|
||||
else:
|
||||
# Create regular files when forced to use mock mode
|
||||
device_path.touch()
|
||||
|
||||
return device_path
|
||||
|
||||
def _set_mock_env_for_devices(self, device_type: str, devices: List[Path]):
|
||||
"""Set mock environment variables for device discovery."""
|
||||
env_var = f"TEST_MOCK_{device_type.upper()}_DEVICES"
|
||||
os.environ[env_var] = ",".join(str(d) for d in devices)
|
||||
|
||||
def get_audio_devices(self) -> DeviceDetector:
|
||||
"""Get audio device paths for testing."""
|
||||
if "audio" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "snd" / "controlC0", 116, 0
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "snd" / "pcmC0D0p", 116, 24
|
||||
),
|
||||
]
|
||||
self._created_devices["audio"] = devices
|
||||
self._set_mock_env_for_devices("audio", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["audio"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_video_devices(self) -> DeviceDetector:
|
||||
"""Get video device paths for testing."""
|
||||
if "video" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "video0", 81, 0),
|
||||
self._create_device(self.temp_path / "video1", 81, 1),
|
||||
]
|
||||
self._created_devices["video"] = devices
|
||||
self._set_mock_env_for_devices("video", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["video"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_input_devices(self) -> DeviceDetector:
|
||||
"""Get input device paths for testing."""
|
||||
if "input" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "input" / "event0", 13, 64
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "input" / "event1", 13, 65
|
||||
),
|
||||
self._create_device(self.temp_path / "input" / "mice", 13, 63),
|
||||
]
|
||||
self._created_devices["input"] = devices
|
||||
self._set_mock_env_for_devices("input", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["input"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_gpu_devices(self) -> DeviceDetector:
|
||||
"""Get GPU device paths for testing."""
|
||||
if "gpu" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "dri" / "renderD128", 226, 128
|
||||
)
|
||||
]
|
||||
self._created_devices["gpu"] = devices
|
||||
self._set_mock_env_for_devices("gpu", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["gpu"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_dvb_devices(self) -> DeviceDetector:
|
||||
"""Get DVB device paths for testing."""
|
||||
if "dvb" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(
|
||||
self.temp_path / "dvb" / "adapter0" / "frontend0", 212, 4
|
||||
),
|
||||
self._create_device(
|
||||
self.temp_path / "dvb" / "adapter0" / "demux0", 212, 5
|
||||
),
|
||||
]
|
||||
self._created_devices["dvb"] = devices
|
||||
self._set_mock_env_for_devices("dvb", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["dvb"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_ttyusb_devices(self) -> DeviceDetector:
|
||||
"""Get USB TTY device paths for testing."""
|
||||
if "ttyUSB" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "ttyUSB0", 188, 0),
|
||||
self._create_device(self.temp_path / "ttyUSB1", 188, 1),
|
||||
]
|
||||
self._created_devices["ttyUSB"] = devices
|
||||
self._set_mock_env_for_devices("ttyUSB", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["ttyUSB"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_radio_devices(self) -> DeviceDetector:
|
||||
"""Get radio device paths for testing."""
|
||||
if "radio" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "radio0", 81, 64),
|
||||
self._create_device(self.temp_path / "radio1", 81, 65),
|
||||
]
|
||||
self._created_devices["radio"] = devices
|
||||
self._set_mock_env_for_devices("radio", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["radio"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
||||
|
||||
def get_ttys_devices(self) -> DeviceDetector:
|
||||
"""Get TTY device paths for testing."""
|
||||
if "ttys" not in self._created_devices:
|
||||
devices = [
|
||||
self._create_device(self.temp_path / "tty0", 4, 0),
|
||||
self._create_device(self.temp_path / "tty1", 4, 1),
|
||||
self._create_device(self.temp_path / "tty2", 4, 2),
|
||||
]
|
||||
self._created_devices["ttys"] = devices
|
||||
self._set_mock_env_for_devices("ttys", devices)
|
||||
|
||||
device_paths = [str(d) for d in self._created_devices["ttys"]]
|
||||
return DeviceDetector(
|
||||
paths=device_paths, expected_count=len(device_paths)
|
||||
)
|
|
@ -0,0 +1,24 @@
|
|||
[pytest]
|
||||
testpaths = .
|
||||
python_files = test_*.py
|
||||
python_classes = Test*
|
||||
python_functions = test_*
|
||||
addopts =
|
||||
--strict-markers
|
||||
--verbose
|
||||
--tb=short
|
||||
markers =
|
||||
unit: Unit tests that do not require external dependencies
|
||||
integration: Integration tests that may require system resources
|
||||
performance: Performance tests with timing requirements
|
||||
slow: Tests that take more than 1 second to run
|
||||
hook_execution: Tests that actually execute OCI hooks
|
||||
json_validation: Tests that validate JSON structure
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
log_cli = true
|
||||
log_cli_level = INFO
|
||||
log_cli_format = %(asctime)s [%(levelname)8s] %(name)s: %(message)s
|
||||
log_cli_date_format = %Y-%m-%d %H:%M:%S
|
||||
junit_family = xunit2
|
|
@ -0,0 +1,5 @@
|
|||
# Basic testing framework
|
||||
pytest>=7.0.0
|
||||
|
||||
# Test fixtures and utilities
|
||||
jsonschema>=4.0.0
|
|
@ -0,0 +1,443 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Tests for OCI hook configuration files."""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from collections import Counter
|
||||
|
||||
import pytest
|
||||
|
||||
from test_utils import HookConfigLoader
|
||||
|
||||
|
||||
TEST_SPEC = {
|
||||
"version": "1.0.0",
|
||||
"process": {
|
||||
"terminal": False,
|
||||
"user": {"uid": 0, "gid": 0},
|
||||
"args": ["echo", "test"],
|
||||
"env": [
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
],
|
||||
"cwd": "/",
|
||||
"capabilities": {
|
||||
"bounding": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"effective": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_KILL",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
],
|
||||
},
|
||||
"rlimits": [{"type": "RLIMIT_NOFILE", "hard": 1024, "soft": 1024}],
|
||||
"noNewPrivileges": True,
|
||||
},
|
||||
"root": {"path": "rootfs", "readonly": True},
|
||||
"hostname": "testing",
|
||||
"mounts": [
|
||||
{"destination": "/proc", "type": "proc", "source": "proc"},
|
||||
{
|
||||
"destination": "/dev",
|
||||
"type": "tmpfs",
|
||||
"source": "tmpfs",
|
||||
"options": ["nosuid", "strictatime", "mode=755", "size=65536k"],
|
||||
},
|
||||
],
|
||||
"annotations": {},
|
||||
"linux": {
|
||||
"resources": {},
|
||||
"namespaces": [
|
||||
{"type": "pid"},
|
||||
{"type": "network"},
|
||||
{"type": "ipc"},
|
||||
{"type": "uts"},
|
||||
{"type": "mount"},
|
||||
],
|
||||
"maskedPaths": [
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
],
|
||||
"readonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger",
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class TestHookConfigurations:
|
||||
"""Test class for validating OCI hook JSON configurations."""
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def hook_configs(self):
|
||||
"""Load all hook configuration files."""
|
||||
return HookConfigLoader.load_all_hook_configs()
|
||||
|
||||
@staticmethod
|
||||
def _get_hook_names():
|
||||
"""Get hook names for parametrized tests."""
|
||||
return HookConfigLoader.get_hook_names()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_json_syntax_validity(self, hook_configs, hook_name):
|
||||
"""Test that hook JSON file has valid syntax."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
# Re-read and parse to ensure JSON is valid
|
||||
try:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
json.load(f)
|
||||
except json.JSONDecodeError as e:
|
||||
pytest.fail(f"Invalid JSON syntax in {config_path}: {e}")
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
@pytest.mark.parametrize(
|
||||
"required_field", ["version", "hook", "when", "stages"]
|
||||
)
|
||||
def test_required_schema_fields(
|
||||
self, hook_configs, hook_name, required_field
|
||||
):
|
||||
"""Test that hook config has required OCI hook schema fields."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
assert (
|
||||
required_field in config
|
||||
), f"Missing required field '{required_field}' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_path_field(self, hook_configs, hook_name):
|
||||
"""Test that hook config has required OCI hook schema field 'path'."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
hook_config = hook_data["config"].get("hook", {})
|
||||
config_path = hook_data["path"]
|
||||
|
||||
assert (
|
||||
"path" in hook_config
|
||||
), f"Missing required hook field 'path' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_version_format(self, hook_configs, hook_name):
|
||||
"""Test that version field follows semantic versioning."""
|
||||
semver_pattern = re.compile(r"^\d+\.\d+\.\d+$")
|
||||
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
version = config.get("version")
|
||||
assert version, f"Version field is empty in {config_path}"
|
||||
assert semver_pattern.match(
|
||||
version
|
||||
), f"Invalid version format '{version}' in {config_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_valid_stages(self, hook_configs, hook_name):
|
||||
"""Test that stages contain only valid OCI hook stages."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
valid_stages = ["prestart", "precreate", "poststart", "poststop"]
|
||||
|
||||
stages = config.get("stages", [])
|
||||
assert isinstance(
|
||||
stages, list
|
||||
), f"Stages must be a list in {config_path}"
|
||||
assert (
|
||||
len(stages) > 0
|
||||
), f"At least one stage must be specified in {config_path}"
|
||||
|
||||
assert all(
|
||||
stage in valid_stages for stage in stages
|
||||
), f"Invalid stages in {config_path}. Valid stages: {valid_stages}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_annotation_patterns(self, hook_configs, hook_name):
|
||||
"""Test that annotation patterns are valid regular expressions."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
when_section = config.get("when", {})
|
||||
annotations = when_section.get("annotations", {})
|
||||
|
||||
def is_valid_regex(pattern):
|
||||
try:
|
||||
re.compile(pattern)
|
||||
return True
|
||||
except re.error:
|
||||
return False
|
||||
|
||||
invalid_patterns = {
|
||||
p: "key" for p in annotations.keys() if not is_valid_regex(p)
|
||||
}
|
||||
invalid_value_patterns = {
|
||||
v: "value"
|
||||
for v in annotations.values()
|
||||
if isinstance(v, str) and not is_valid_regex(v)
|
||||
}
|
||||
|
||||
assert not invalid_patterns, (
|
||||
f"Invalid regex in annotation keys: {invalid_patterns} "
|
||||
f"in {config_path}"
|
||||
)
|
||||
assert not invalid_value_patterns, (
|
||||
f"Invalid regex in annotation values: {invalid_value_patterns} "
|
||||
f"in {config_path}"
|
||||
)
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_executable_exists(self, hook_configs, hook_name):
|
||||
"""Test that hook executables exist and are executable."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
hook_path = Path(hook_data["config"]["hook"]["path"])
|
||||
|
||||
# For tests, the hook might be in the source directory rather than
|
||||
# installed location - try absolute path first, then relative
|
||||
try:
|
||||
# Check if absolute path works
|
||||
hook_path.stat()
|
||||
except OSError:
|
||||
# If absolute path fails, try relative to config file
|
||||
hook_path = config_path.parent / hook_path.name
|
||||
|
||||
assert hook_path.exists(), f"Hook executable not found: {hook_path}"
|
||||
assert hook_path.is_file(), f"Hook executable not a file: {hook_path}"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_device_manager_annotations(self, hook_configs):
|
||||
"""Test that device manager config matches supported annotations."""
|
||||
hook_name = "oci-qm-device-manager"
|
||||
assert hook_name in hook_configs, f"Hook {hook_name} not found"
|
||||
|
||||
device_manager_config = hook_configs[hook_name]["config"]
|
||||
annotations = device_manager_config.get("when", {}).get(
|
||||
"annotations", {}
|
||||
)
|
||||
|
||||
# Check that device patterns exist
|
||||
device_patterns = [p for p in annotations.keys() if "device" in p]
|
||||
assert (
|
||||
device_patterns
|
||||
), "Device manager should support device annotations"
|
||||
|
||||
# Check that wayland patterns exist
|
||||
wayland_patterns = [p for p in annotations.keys() if "wayland" in p]
|
||||
assert (
|
||||
wayland_patterns
|
||||
), "Device manager should support Wayland annotations"
|
||||
|
||||
# Test that device patterns match expected device types
|
||||
test_annotations = [
|
||||
"org.containers.qm.device.audio",
|
||||
"org.containers.qm.device.video",
|
||||
"org.containers.qm.device.input",
|
||||
"org.containers.qm.device.ttys",
|
||||
]
|
||||
|
||||
unmatched_patterns = [
|
||||
p
|
||||
for p in device_patterns
|
||||
if not all(re.match(p, a) for a in test_annotations)
|
||||
]
|
||||
|
||||
assert not unmatched_patterns, (
|
||||
f"Device patterns {unmatched_patterns} failed to match all "
|
||||
"expected annotations"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wayland_client_annotations(self, hook_configs):
|
||||
"""Test that wayland client config matches supported annotations."""
|
||||
hook_name = "oci-qm-wayland-client-devices"
|
||||
assert hook_name in hook_configs, f"Hook {hook_name} not found"
|
||||
|
||||
wayland_config = hook_configs[hook_name]["config"]
|
||||
annotations = wayland_config.get("when", {}).get("annotations", {})
|
||||
|
||||
# Check that wayland-client patterns exist
|
||||
wayland_patterns = [
|
||||
p for p in annotations.keys() if "wayland-client" in p
|
||||
]
|
||||
assert (
|
||||
wayland_patterns
|
||||
), "Wayland client should have wayland-client pattern"
|
||||
|
||||
# Test that patterns match expected annotations
|
||||
test_annotations = ["org.containers.qm.wayland-client.gpu"]
|
||||
|
||||
unmatched_patterns = [
|
||||
p
|
||||
for p in wayland_patterns
|
||||
if not all(re.match(p, a) for a in test_annotations)
|
||||
]
|
||||
|
||||
assert not unmatched_patterns, (
|
||||
f"Wayland patterns {unmatched_patterns} failed to match all "
|
||||
"expected annotations"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_no_duplicate_annotations(self, hook_configs):
|
||||
"""Test that there are no overlapping annotation patterns."""
|
||||
# Create a flat list of all annotation patterns from all hooks
|
||||
all_patterns = [
|
||||
pattern
|
||||
for hook_data in hook_configs.values()
|
||||
for pattern in hook_data["config"]
|
||||
.get("when", {})
|
||||
.get("annotations", {})
|
||||
.keys()
|
||||
]
|
||||
|
||||
# Count occurrences of each pattern
|
||||
pattern_counts = Counter(all_patterns)
|
||||
|
||||
# Assert no duplicate patterns exist using named expression
|
||||
assert not (
|
||||
duplicates := {
|
||||
pattern: count
|
||||
for pattern, count in pattern_counts.items()
|
||||
if count > 1
|
||||
}
|
||||
), "Duplicate annotation patterns found: {}".format( # pylint: disable=C0209 # noqa: E501
|
||||
{
|
||||
pattern: [
|
||||
name
|
||||
for name, data in hook_configs.items()
|
||||
if pattern
|
||||
in data["config"].get("when", {}).get("annotations", {})
|
||||
]
|
||||
for pattern in duplicates
|
||||
}
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_json_formatting(self, hook_configs, hook_name):
|
||||
"""Test that JSON file is properly formatted."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config_path = hook_data["path"]
|
||||
config = hook_data["config"]
|
||||
|
||||
# Read the original file content
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
original_content = f.read()
|
||||
|
||||
# Generate formatted JSON
|
||||
_ = json.dumps(config, indent=2, sort_keys=False) + "\n"
|
||||
|
||||
# Check if formatting is consistent (allowing for minor variations)
|
||||
# This is a basic check - in practice you might want to be
|
||||
# more lenient
|
||||
assert (
|
||||
len(original_content.strip()) > 0
|
||||
), f"JSON file {config_path} appears to be empty"
|
||||
|
||||
# Check that it's valid JSON by trying to parse it
|
||||
try:
|
||||
json.loads(original_content)
|
||||
except json.JSONDecodeError as e:
|
||||
pytest.fail(f"Invalid JSON in {config_path}: {e}")
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_hook_config_integration(self, hook_configs, hook_name):
|
||||
"""Test that hook config integrates correctly with its executable."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
config_path = hook_data["path"]
|
||||
|
||||
# Get hook executable path
|
||||
hook_path = config.get("hook", {}).get("path", "")
|
||||
assert hook_path, f"Hook path not found in {config_path}"
|
||||
|
||||
# Try absolute path first, then relative to config directory
|
||||
full_hook_path = Path(hook_path)
|
||||
try:
|
||||
# Check if absolute path works
|
||||
full_hook_path.stat()
|
||||
except OSError:
|
||||
# If absolute path fails, try relative to config directory
|
||||
full_hook_path = config_path.parent / Path(hook_path).name
|
||||
|
||||
# Check that executable exists
|
||||
assert full_hook_path.exists(), (
|
||||
f"Hook executable not found: {full_hook_path} "
|
||||
f"(configured in {config_path})"
|
||||
)
|
||||
|
||||
# Check that it's executable
|
||||
assert (
|
||||
full_hook_path.is_file()
|
||||
), f"Hook path is not a file: {full_hook_path}"
|
||||
|
||||
# Basic executable check (if we can check permissions)
|
||||
try:
|
||||
assert (
|
||||
full_hook_path.stat().st_mode & 0o111
|
||||
), f"Hook file is not executable: {full_hook_path}"
|
||||
except OSError:
|
||||
# Skip permission check if we can't access file stats
|
||||
pass
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("hook_name", _get_hook_names())
|
||||
def test_stage_appropriateness(self, hook_configs, hook_name):
|
||||
"""Test that hook uses appropriate stages for its functionality."""
|
||||
hook_data = hook_configs[hook_name]
|
||||
config = hook_data["config"]
|
||||
stages = config.get("stages", [])
|
||||
|
||||
# Device management hooks should use precreate stage
|
||||
# because they need to modify the OCI spec before container creation
|
||||
assert "precreate" in stages, (
|
||||
f"Hook {hook_name} should use 'precreate' stage for device "
|
||||
"management"
|
||||
)
|
||||
|
||||
# Should not use poststop for device hooks
|
||||
assert (
|
||||
"poststop" not in stages
|
||||
), f"Device hook {hook_name} should not use 'poststop' stage"
|
|
@ -0,0 +1,107 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Basic functionality tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import DeviceCounter
|
||||
|
||||
|
||||
class TestQMOCIHooksBasic:
|
||||
"""Basic functionality tests for QM device manager."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"hook_name", ["qm_device_manager", "wayland_client_devices"]
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"json_spec",
|
||||
[
|
||||
{"annotations": {}, "linux": {}},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.unknown": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": "false"},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["empty_spec", "unknown_spec", "invalid_spec"],
|
||||
)
|
||||
def test_basic_annotations(
|
||||
self, hook_name, json_spec, hook_runner, oci_spec_validator
|
||||
):
|
||||
"""Test hook with basic annotations returns unchanged spec."""
|
||||
result = hook_runner.run_hook(hook_name, json_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output spec is invalid"
|
||||
assert (
|
||||
DeviceCounter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"invalid_spec",
|
||||
[
|
||||
# Missing required 'linux' property
|
||||
{"annotations": {"com.example": "value"}},
|
||||
# 'annotations' should be an object, not a string
|
||||
{"annotations": "not-an-object", "linux": {}},
|
||||
# Add a property not defined in the schema
|
||||
{
|
||||
"annotations": {"com.example": "value"},
|
||||
"linux": {},
|
||||
"extra": 123,
|
||||
},
|
||||
],
|
||||
)
|
||||
def test_invalid_annotations(self, oci_spec_validator, invalid_spec):
|
||||
"""Test invalid annotations are rejected."""
|
||||
assert not oci_spec_validator(
|
||||
invalid_spec
|
||||
), "Invalid spec should be rejected"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_missing_linux_section(self, hook_runner):
|
||||
"""Test hook creates linux section if missing."""
|
||||
malformed_json = {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"}
|
||||
# Missing linux section
|
||||
}
|
||||
result = hook_runner.run_hook("qm_device_manager", malformed_json)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
"linux" in result.output_spec
|
||||
), "Hook should create linux section"
|
||||
assert isinstance(
|
||||
result.output_spec["linux"], dict
|
||||
), "Linux section should be a dict"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"json_spec",
|
||||
[
|
||||
# Non-wayland annotations ignored
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {},
|
||||
},
|
||||
# Unknown wayland annotations ignored
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland-client.unknown": "true"
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["non_wayland_ignored", "unknown_wayland_ignored"],
|
||||
)
|
||||
def test_wayland_annotations(self, hook_runner, json_spec, device_counter):
|
||||
"""Test wayland annotations are processed correctly."""
|
||||
result = hook_runner.run_hook("wayland_client_devices", json_spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
device_counter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added"
|
|
@ -0,0 +1,155 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Device detection and annotation tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
from conftest import DeviceCounter
|
||||
|
||||
|
||||
class TestQMDeviceManagerDevices:
|
||||
"""Device annotation tests for QM device manager."""
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.hook_execution
|
||||
def test_audio_device_annotation(
|
||||
self, hook_runner, sample_specs, device_counter
|
||||
):
|
||||
"""Test audio device annotation executes successfully."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Hook should succeed and produce valid output regardless of
|
||||
# available devices (device-specific testing in temp_devices tests)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, "Device count should be non-negative"
|
||||
|
||||
@pytest.mark.integration
|
||||
@pytest.mark.hook_execution
|
||||
def test_multiple_device_annotations(
|
||||
self, hook_runner, sample_specs, device_counter
|
||||
):
|
||||
"""Test multiple device annotations."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should have devices for at least one of the requested types
|
||||
# (depending on what's available in test environment)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
|
||||
# Even if no devices are available, hook should succeed
|
||||
assert total_devices >= 0, "Device count should be non-negative"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"device_type,annotation_key",
|
||||
[
|
||||
("audio", "org.containers.qm.device.audio"),
|
||||
("video", "org.containers.qm.device.video"),
|
||||
("input", "org.containers.qm.device.input"),
|
||||
("ttys", "org.containers.qm.device.ttys"),
|
||||
("ttyUSB", "org.containers.qm.device.ttyUSB"),
|
||||
("dvb", "org.containers.qm.device.dvb"),
|
||||
("radio", "org.containers.qm.device.radio"),
|
||||
],
|
||||
)
|
||||
def test_individual_device_types(
|
||||
self, hook_runner, device_type, annotation_key
|
||||
):
|
||||
"""Test individual device type annotations."""
|
||||
spec = {"annotations": {annotation_key: "true"}, "linux": {}}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed for {device_type}: {result.stderr}"
|
||||
|
||||
# Validate result.output_spec structure
|
||||
assert "linux" in result.output_spec
|
||||
assert ("devices" in result.output_spec.get("linux", {})) or (
|
||||
DeviceCounter.count_devices(result.output_spec) == 0
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_audio_device_annotation_with_temp_devices(
|
||||
self, hook_runner, sample_specs, device_counter, temp_devices
|
||||
):
|
||||
"""Test audio device annotation with specific device creation."""
|
||||
device_detector = temp_devices.get_audio_devices()
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager",
|
||||
sample_specs["audio_device"],
|
||||
mock_devices=device_detector.mock_devices(device_type="audio"),
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Verify expected number of devices
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} audio devices, "
|
||||
f"got {device_count}"
|
||||
)
|
||||
|
||||
# Verify the specific temporary devices are present
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
found_paths = [d["path"] for d in devices]
|
||||
|
||||
assert all(
|
||||
temp_path in found_paths for temp_path in device_detector.paths
|
||||
), (
|
||||
f"All temporary audio devices should be found in output. "
|
||||
f"Expected: {device_detector.paths}, Found: {found_paths}"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"device_type,device_method",
|
||||
[
|
||||
("video", "get_video_devices"),
|
||||
("input", "get_input_devices"),
|
||||
("dvb", "get_dvb_devices"),
|
||||
("ttyUSB", "get_ttyusb_devices"),
|
||||
("radio", "get_radio_devices"),
|
||||
("ttys", "get_ttys_devices"),
|
||||
],
|
||||
)
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
def test_device_annotation_with_temp_devices(
|
||||
self,
|
||||
hook_runner,
|
||||
device_counter,
|
||||
temp_devices,
|
||||
device_type,
|
||||
device_method,
|
||||
):
|
||||
"""Test various device types with specific device creation."""
|
||||
device_detector = getattr(temp_devices, device_method)()
|
||||
|
||||
annotation_key = f"org.containers.qm.device.{device_type}"
|
||||
spec = {
|
||||
"annotations": {annotation_key: "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager",
|
||||
spec,
|
||||
mock_devices=device_detector.mock_devices(device_type),
|
||||
)
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed for {device_type}: {result.stderr}"
|
||||
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} {device_type} "
|
||||
f"devices, got {device_count}"
|
||||
)
|
|
@ -0,0 +1,134 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Error handling and edge case tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerErrorHandling:
|
||||
"""Error handling and edge case tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_non_qm_annotations_ignored(self, hook_runner, device_counter):
|
||||
"""Test that non-QM annotations are ignored."""
|
||||
spec = {
|
||||
"annotations": {
|
||||
"org.opencontainers.image.title": "test",
|
||||
"com.example.custom": "value",
|
||||
},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert (
|
||||
device_counter.count_devices(result.output_spec) == 0
|
||||
), "No devices should be added for non-QM annotations"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_hook_with_existing_devices(self, hook_runner):
|
||||
"""Test hook behavior when devices already exist."""
|
||||
spec_with_devices = {
|
||||
"annotations": {"org.containers.qm.device.audio": "true"},
|
||||
"linux": {
|
||||
"devices": [
|
||||
{
|
||||
"path": "/dev/existing",
|
||||
"type": "c",
|
||||
"major": 1,
|
||||
"minor": 3,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_devices)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing devices and potentially add new ones
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
existing_paths = [d["path"] for d in devices]
|
||||
assert (
|
||||
"/dev/existing" in existing_paths
|
||||
), "Existing device should be preserved"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_hook_with_existing_resources(self, hook_runner):
|
||||
"""Test hook behavior when resources already exist."""
|
||||
spec_with_resources = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {
|
||||
"resources": {"devices": [{"allow": False, "type": "a"}]}
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_resources)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing resources
|
||||
resources = result.output_spec.get("linux", {}).get("resources", {})
|
||||
devices_limit = resources.get("devices", [])
|
||||
|
||||
# Check that the existing rule is preserved
|
||||
deny_all_rule = {"allow": False, "type": "a"}
|
||||
assert (
|
||||
deny_all_rule in devices_limit
|
||||
), "Existing resource rules should be preserved"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesErrorHandling:
|
||||
"""Error handling tests for Wayland client devices."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_annotation_isolation(self, hook_runner, device_counter):
|
||||
"""Test that invalid annotations don't affect valid ones."""
|
||||
spec = {
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland-client.gpu": "true",
|
||||
"org.containers.qm.wayland-client.invalid": "bad_value",
|
||||
"org.opencontainers.image.title": "test",
|
||||
},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Valid GPU annotation should still be processed
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, "Valid annotations should be processed"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_annotation_value(self, hook_runner, device_counter):
|
||||
"""Test handling of empty annotation values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": ""},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Empty value should be treated as falsy
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), "Empty annotation value should not add devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_unknown_annotation_values(self, hook_runner, device_counter):
|
||||
"""Test handling of unknown annotation values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": "maybe"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Unknown values should typically be treated as falsy for safety
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), "Unknown annotation values should not add devices"
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Logging tests for QM device manager and Wayland client devices."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerLogging:
|
||||
"""Logging functionality tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_file_creation(self, hook_runner, log_checker):
|
||||
"""Test that hook creates log file."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
result = hook_runner.run_hook("qm_device_manager", empty_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker("qm-device-manager"), "Log should contain hook name"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_content_validation(
|
||||
self, hook_runner, sample_specs, log_checker
|
||||
):
|
||||
"""Test that hook logs contain expected content."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"QM Device Manager"
|
||||
), "Log should contain hook description"
|
||||
assert log_checker("completed successfully")
|
||||
|
||||
|
||||
class TestWaylandClientDevicesLogging:
|
||||
"""Logging functionality tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_file_creation(self, hook_runner, log_checker):
|
||||
"""Test that hook creates log file."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
result = hook_runner.run_hook("wayland_client_devices", empty_spec)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"qm-wayland-client-devices"
|
||||
), "Log should contain hook name"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.hook_execution
|
||||
def test_log_content_validation(
|
||||
self, hook_runner, sample_specs, log_checker
|
||||
):
|
||||
"""Test that hook logs contain expected content."""
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert log_checker(
|
||||
"qm-wayland-client-devices"
|
||||
), "Log should contain hook name"
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Performance tests for QM device manager and Wayland client devices."""
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerPerformance:
|
||||
"""Performance and timing tests."""
|
||||
|
||||
@pytest.mark.performance
|
||||
def test_hook_performance(self, hook_runner, sample_specs):
|
||||
"""Test that hook executes within reasonable time."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
# Hook should complete within 5 seconds (generous limit)
|
||||
assert (
|
||||
execution_time < 5.0
|
||||
), f"Hook took too long: {execution_time:.2f}s"
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.parametrize("iteration", range(5))
|
||||
def test_multiple_executions_performance(
|
||||
self, hook_runner, sample_specs, iteration
|
||||
):
|
||||
"""Test consistent performance across multiple executions."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed on iteration {iteration}: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 3.0
|
||||
), f"Execution {iteration} too slow: {execution_time:.2f}s"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesPerformance:
|
||||
"""Performance tests for Wayland client devices."""
|
||||
|
||||
@pytest.mark.performance
|
||||
def test_hook_performance(self, hook_runner, sample_specs):
|
||||
"""Test Wayland client devices hook performance."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 3.0
|
||||
), f"Hook took too long: {execution_time:.2f}s"
|
||||
|
||||
@pytest.mark.performance
|
||||
@pytest.mark.parametrize("iteration", range(3))
|
||||
def test_multiple_executions_performance(
|
||||
self, hook_runner, sample_specs, iteration
|
||||
):
|
||||
"""Test consistent performance across multiple executions."""
|
||||
start_time = time.time()
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
end_time = time.time()
|
||||
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed on iteration {iteration}: {result.stderr}"
|
||||
|
||||
execution_time = end_time - start_time
|
||||
assert (
|
||||
execution_time < 2.0
|
||||
), f"Execution {iteration} too slow: {execution_time:.2f}s"
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validation tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerValidation:
|
||||
"""JSON and structure validation tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_output_json_structure(
|
||||
self, hook_runner, sample_specs, oci_spec_validator
|
||||
):
|
||||
"""Test that hook output follows OCI specification structure."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["audio_device"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output spec is invalid"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_device_structure_validation(self, hook_runner, sample_specs):
|
||||
"""Test that device entries have correct structure."""
|
||||
result = hook_runner.run_hook(
|
||||
"qm_device_manager", sample_specs["multiple_devices"]
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
|
||||
# Check that all devices have required fields
|
||||
required_fields = ["path", "type", "major", "minor"]
|
||||
assert all(
|
||||
field in device for device in devices for field in required_fields
|
||||
), "All devices must have required fields: path, type, major, minor"
|
||||
|
||||
# Check path types
|
||||
assert all(
|
||||
isinstance(d["path"], str) for d in devices
|
||||
), "Path must be str"
|
||||
# Check type values
|
||||
assert all(
|
||||
d["type"] in ["c", "b"] for d in devices
|
||||
), "Type must be c or b"
|
||||
# Check major/minor are integers
|
||||
assert all(
|
||||
isinstance(d["major"], int) for d in devices
|
||||
), "Major must be int"
|
||||
assert all(
|
||||
isinstance(d["minor"], int) for d in devices
|
||||
), "Minor must be int"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_resource_structure_validation(self, hook_runner):
|
||||
"""Test that resource entries have correct structure."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Check devices limit structure
|
||||
devices_limit = (
|
||||
result.output_spec.get("linux", {})
|
||||
.get("resources", {})
|
||||
.get("devices", [])
|
||||
)
|
||||
|
||||
# Validate all device rules have required fields
|
||||
assert all(
|
||||
"allow" in device_rule for device_rule in devices_limit
|
||||
), "All device rules must have 'allow' field"
|
||||
|
||||
assert all(
|
||||
"type" in device_rule for device_rule in devices_limit
|
||||
), "All device rules must have 'type' field"
|
||||
|
||||
assert all(
|
||||
isinstance(device_rule["allow"], bool)
|
||||
for device_rule in devices_limit
|
||||
), "All 'allow' fields must be bool"
|
||||
|
||||
assert all(
|
||||
device_rule["type"] in ["c", "b", "a"]
|
||||
for device_rule in devices_limit
|
||||
), "All device types must be 'c', 'b', or 'a'"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_empty_annotations_validation(
|
||||
self, hook_runner, oci_spec_validator
|
||||
):
|
||||
"""Test validation with empty annotations."""
|
||||
empty_spec = {"annotations": {}, "linux": {}}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", empty_spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
assert oci_spec_validator(result.output_spec), "Output should be valid"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"malformed_spec",
|
||||
[
|
||||
# Non-string annotation values should be handled gracefully
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": True},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": 123},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {"org.containers.qm.device.audio": []},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["boolean_value", "integer_value", "list_value"],
|
||||
)
|
||||
def test_malformed_annotation_values(self, hook_runner, malformed_spec):
|
||||
"""Test handling of malformed annotation values."""
|
||||
result = hook_runner.run_hook("qm_device_manager", malformed_spec)
|
||||
# Hook should not crash, but may not process the annotation
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook should handle malformed values: {result.stderr}"
|
|
@ -0,0 +1,97 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wayland seat functionality tests for QM device manager."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestQMDeviceManagerWayland:
|
||||
"""Wayland seat annotation tests."""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"seat_annotation",
|
||||
[
|
||||
"org.containers.qm.wayland.seat",
|
||||
"org.containers.qm.wayland.seat.0",
|
||||
"org.containers.qm.wayland.seat.1",
|
||||
],
|
||||
)
|
||||
def test_wayland_seat_annotation(
|
||||
self, hook_runner, seat_annotation, device_counter
|
||||
):
|
||||
"""Test Wayland seat annotations."""
|
||||
spec = {
|
||||
"annotations": {seat_annotation: "true"},
|
||||
"linux": {},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Verify devices and resources are added for seat
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
resource_count = device_counter.count_resources(result.output_spec)
|
||||
|
||||
assert device_count >= 0, "Device count should be non-negative"
|
||||
assert resource_count >= 0, "Resource count should be non-negative"
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_wayland_seat_with_existing_devices(self, hook_runner):
|
||||
"""Test wayland seat annotation with existing devices."""
|
||||
spec_with_devices = {
|
||||
"annotations": {"org.containers.qm.wayland.seat": "true"},
|
||||
"linux": {
|
||||
"devices": [
|
||||
{
|
||||
"path": "/dev/existing",
|
||||
"type": "c",
|
||||
"major": 1,
|
||||
"minor": 3,
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
result = hook_runner.run_hook("qm_device_manager", spec_with_devices)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should preserve existing devices
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
existing_paths = [d["path"] for d in devices]
|
||||
assert (
|
||||
"/dev/existing" in existing_paths
|
||||
), "Existing device should be preserved"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"spec",
|
||||
[
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland.seat.0": "true",
|
||||
"org.containers.qm.wayland.seat.1": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
{
|
||||
"annotations": {
|
||||
"org.containers.qm.wayland.seat": "seat0",
|
||||
"org.containers.qm.wayland.seat.0": "true",
|
||||
"org.containers.qm.wayland.seat.1": "true",
|
||||
},
|
||||
"linux": {},
|
||||
},
|
||||
],
|
||||
ids=["multiple_seats", "conflicting_seats"],
|
||||
)
|
||||
def test_multiple_wayland_seats(self, hook_runner, device_counter, spec):
|
||||
"""Test multiple Wayland seat annotations."""
|
||||
result = hook_runner.run_hook("qm_device_manager", spec)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
# Should process both seats
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
resource_count = device_counter.count_resources(result.output_spec)
|
||||
|
||||
assert device_count >= 0, "Should handle multiple seats"
|
||||
assert resource_count >= 0, "Resource count should be non-negative"
|
|
@ -0,0 +1,238 @@
|
|||
"""Shared test utilities for OCI hooks testing."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import jsonschema
|
||||
|
||||
|
||||
@dataclass
|
||||
class HookResult:
|
||||
"""Result of running an OCI hook."""
|
||||
|
||||
success: bool
|
||||
output_spec: Dict[str, Any]
|
||||
stdout: str
|
||||
stderr: str
|
||||
execution_time: float
|
||||
|
||||
|
||||
class HookRunner:
|
||||
"""Class for running OCI hooks in test environment."""
|
||||
|
||||
def __init__(self, test_env: Dict[str, str]):
|
||||
"""Initialize with test environment variables."""
|
||||
self.test_env = test_env
|
||||
|
||||
def run_hook(
|
||||
self,
|
||||
hook_name: str,
|
||||
input_spec: Dict[str, Any],
|
||||
mock_devices: Optional[Dict[str, str]] = None,
|
||||
) -> HookResult:
|
||||
"""Run a hook with given input spec and return result."""
|
||||
hook_paths = {
|
||||
"qm_device_manager": "../qm-device-manager/oci-qm-device-manager",
|
||||
"wayland_client_devices": (
|
||||
"../wayland-client-devices/oci-qm-wayland-client-devices"
|
||||
),
|
||||
}
|
||||
|
||||
if hook_name not in hook_paths:
|
||||
raise ValueError(f"Unknown hook: {hook_name}")
|
||||
|
||||
hook_path = hook_paths[hook_name]
|
||||
input_json = json.dumps(input_spec)
|
||||
|
||||
# Use provided test environment (including TEST_LOGFILE)
|
||||
env = self.test_env.copy()
|
||||
|
||||
# Add mock device environment variables if provided
|
||||
if mock_devices:
|
||||
for device_type, device_list in mock_devices.items():
|
||||
env_var = f"TEST_MOCK_{device_type.upper()}_DEVICES"
|
||||
env[env_var] = device_list
|
||||
|
||||
start_time = time.time()
|
||||
result = subprocess.run(
|
||||
[hook_path],
|
||||
input=input_json,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
cwd=Path(__file__).parent,
|
||||
check=False,
|
||||
)
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
# Parse output JSON if hook succeeded
|
||||
output_spec = {}
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
try:
|
||||
output_spec = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
# If output is not valid JSON, treat as failure
|
||||
pass
|
||||
|
||||
return HookResult(
|
||||
success=result.returncode == 0,
|
||||
output_spec=output_spec,
|
||||
stdout=result.stdout,
|
||||
stderr=result.stderr,
|
||||
execution_time=execution_time,
|
||||
)
|
||||
|
||||
|
||||
class HookConfigLoader:
|
||||
"""Utility for loading and managing hook configurations."""
|
||||
|
||||
@staticmethod
|
||||
def load_all_hook_configs() -> Dict[str, Dict[str, Any]]:
|
||||
"""Load all hook configuration files."""
|
||||
hook_dir = Path(__file__).parent.parent
|
||||
|
||||
def _load_config(json_file):
|
||||
with open(json_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
return {
|
||||
json_file.stem: {
|
||||
"path": json_file,
|
||||
"config": _load_config(json_file),
|
||||
}
|
||||
for json_file in hook_dir.rglob("*.json")
|
||||
if "oci-qm-" in json_file.name
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_hook_names() -> list:
|
||||
"""Get list of all hook names."""
|
||||
return list(HookConfigLoader.load_all_hook_configs().keys())
|
||||
|
||||
|
||||
class OciSpecValidator:
|
||||
"""JSON schema validator for OCI specifications."""
|
||||
|
||||
# Simplified OCI spec schema for validation
|
||||
OCI_SCHEMA = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"annotations": {
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
},
|
||||
"linux": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"type": {"type": "string"},
|
||||
"major": {"type": "integer"},
|
||||
"minor": {"type": "integer"},
|
||||
"fileMode": {"type": "integer"},
|
||||
"uid": {"type": "integer"},
|
||||
"gid": {"type": "integer"},
|
||||
},
|
||||
"required": ["path", "type", "major", "minor"],
|
||||
},
|
||||
},
|
||||
"resources": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"devices": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allow": {"type": "boolean"},
|
||||
"type": {"type": "string"},
|
||||
"major": {"type": "integer"},
|
||||
"minor": {"type": "integer"},
|
||||
"access": {"type": "string"},
|
||||
},
|
||||
"required": [
|
||||
"allow",
|
||||
"type",
|
||||
"major",
|
||||
"minor",
|
||||
"access",
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
"required": ["annotations", "linux"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate(cls, spec: Dict[str, Any]) -> bool:
|
||||
"""Validate an OCI spec against the schema."""
|
||||
try:
|
||||
jsonschema.validate(spec, cls.OCI_SCHEMA)
|
||||
return True
|
||||
except jsonschema.ValidationError:
|
||||
return False
|
||||
|
||||
|
||||
class LogChecker:
|
||||
"""Utility for checking hook log files."""
|
||||
|
||||
def __init__(self, log_file_path: str):
|
||||
"""Initialize with log file path."""
|
||||
self.log_file_path = Path(log_file_path)
|
||||
|
||||
def __call__(self, pattern: str, should_exist: bool = True) -> bool:
|
||||
"""
|
||||
Check if pattern exists in hook log file (backward compatibility).
|
||||
|
||||
Args:
|
||||
pattern: String pattern to search for
|
||||
should_exist: Whether pattern should exist (True) or not (False)
|
||||
|
||||
Returns:
|
||||
True if check passes, False otherwise
|
||||
"""
|
||||
if not self.log_exists():
|
||||
return not should_exist
|
||||
|
||||
try:
|
||||
content = self.log_file_path.read_text(encoding="utf-8")
|
||||
pattern_found = pattern in content
|
||||
return pattern_found if should_exist else not pattern_found
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def log_exists(self) -> bool:
|
||||
"""Check if log file exists."""
|
||||
return self.log_file_path.exists()
|
||||
|
||||
def log_contains(self, text: str) -> bool:
|
||||
"""Check if log contains specific text."""
|
||||
if not self.log_exists():
|
||||
return False
|
||||
content = self.log_file_path.read_text(encoding="utf-8")
|
||||
return text in content
|
||||
|
||||
def get_log_lines(self) -> list:
|
||||
"""Get all log lines as a list."""
|
||||
try:
|
||||
return (
|
||||
self.log_file_path.read_text(encoding="utf-8")
|
||||
.strip()
|
||||
.split("\n")
|
||||
)
|
||||
except OSError:
|
||||
return []
|
|
@ -0,0 +1,178 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Wayland GPU device tests."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestWaylandClientDevicesGPU:
|
||||
"""GPU device detection tests for Wayland client containers."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_annotation_enabled(self, hook_runner, sample_specs):
|
||||
"""Test GPU annotation processes correctly."""
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices", sample_specs["wayland_gpu"]
|
||||
)
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_value", ["true", "1", "yes", "True", "YES"])
|
||||
def test_gpu_annotation_valid_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with various valid truthy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For truthy values, devices should be processed
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices >= 0
|
||||
), f"Truthy value '{gpu_value}' should enable GPU processing"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize("gpu_value", ["false", "0", "no", "False", "NO"])
|
||||
def test_gpu_annotation_invalid_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with various falsy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with falsy value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For falsy values, no devices should be added
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Falsy value '{gpu_value}' should not add GPU devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value", ["TrUe", "YeS", "tRuE", "yEs", "TRUE"]
|
||||
)
|
||||
def test_gpu_annotation_case_insensitive_truthy_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with mixed-case truthy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with mixed-case value '{gpu_value}': {result.stderr}"
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert total_devices >= 0, (
|
||||
f"Mixed-case truthy value '{gpu_value}' should enable GPU "
|
||||
"processing"
|
||||
)
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value", ["FaLsE", "nO", "fAlSe", "No", "FALSE"]
|
||||
)
|
||||
def test_gpu_annotation_case_insensitive_falsy_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with mixed-case falsy values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
assert (
|
||||
result.success
|
||||
), f"Hook failed with mixed-case value '{gpu_value}': {result.stderr}"
|
||||
|
||||
# For falsy values, no devices should be added
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Mixed-case falsy value '{gpu_value}' should not add GPU devices"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.parametrize(
|
||||
"gpu_value",
|
||||
[
|
||||
"2", # Number other than 1
|
||||
"42", # Random number
|
||||
"maybe", # Unrelated string
|
||||
"truthy", # Similar but not exact
|
||||
"YES_PLEASE", # Contains valid word but extra chars
|
||||
"null", # Null-like value
|
||||
"", # Empty string
|
||||
"[]", # JSON array string
|
||||
"{}", # JSON object string
|
||||
"true false", # Multiple values
|
||||
"TRUE;true", # Semicolon separated
|
||||
"on", # Common boolean-like value
|
||||
"off", # Common boolean-like value
|
||||
"enable", # Enable-like value
|
||||
"disable", # Disable-like value
|
||||
],
|
||||
)
|
||||
def test_gpu_annotation_malformed_values(
|
||||
self, hook_runner, device_counter, gpu_value
|
||||
):
|
||||
"""Test GPU annotation with malformed or unexpected values."""
|
||||
spec = {
|
||||
"annotations": {"org.containers.qm.wayland-client.gpu": gpu_value},
|
||||
"linux": {},
|
||||
}
|
||||
result = hook_runner.run_hook("wayland_client_devices", spec)
|
||||
|
||||
# Hook should handle malformed values gracefully without failing
|
||||
assert result.success, (
|
||||
f"Hook should not fail with malformed value '{gpu_value}': "
|
||||
f"{result.stderr}"
|
||||
)
|
||||
|
||||
# Malformed values should be treated as falsy (no GPU devices added)
|
||||
total_devices = device_counter.count_devices(result.output_spec)
|
||||
assert (
|
||||
total_devices == 0
|
||||
), f"Malformed value '{gpu_value}' should not enable GPU processing"
|
||||
|
||||
|
||||
class TestWaylandClientDevicesGPUDevices:
|
||||
"""GPU device tests for Wayland client containers."""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_gpu_annotation(
|
||||
self, hook_runner, sample_specs, device_counter, temp_devices
|
||||
):
|
||||
"""Test GPU annotation with temp devices."""
|
||||
device_detector = temp_devices.get_gpu_devices()
|
||||
|
||||
result = hook_runner.run_hook(
|
||||
"wayland_client_devices",
|
||||
sample_specs["wayland_gpu"],
|
||||
mock_devices=device_detector.mock_devices("gpu"),
|
||||
)
|
||||
|
||||
assert result.success, f"Hook failed: {result.stderr}"
|
||||
|
||||
device_count = device_counter.count_devices(result.output_spec)
|
||||
assert device_count == device_detector.expected_count, (
|
||||
f"Expected {device_detector.expected_count} GPU devices, "
|
||||
f"got {device_count}"
|
||||
)
|
||||
|
||||
devices = result.output_spec.get("linux", {}).get("devices", [])
|
||||
found_paths = [d["path"] for d in devices]
|
||||
|
||||
assert all(
|
||||
temp_path in found_paths for temp_path in device_detector.paths
|
||||
), "Temporary GPU device not found in output"
|
|
@ -0,0 +1,82 @@
|
|||
[tox]
|
||||
envlist = unit
|
||||
skipsdist = true
|
||||
|
||||
[testenv:unit]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m unit --tb=short -v {posargs}
|
||||
|
||||
[testenv:integration]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m integration --tb=short -v {posargs}
|
||||
|
||||
[testenv:performance]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest -m performance --tb=short -v {posargs}
|
||||
|
||||
[testenv:all]
|
||||
deps = -r tests/requirements.txt
|
||||
allowlist_externals = chmod
|
||||
passenv = FORCE_MOCK_DEVICES
|
||||
changedir = tests
|
||||
commands_pre =
|
||||
chmod +x {toxinidir}/qm-device-manager/oci-qm-device-manager
|
||||
chmod +x {toxinidir}/wayland-client-devices/oci-qm-wayland-client-devices
|
||||
commands =
|
||||
pytest --tb=short -v {posargs}
|
||||
|
||||
[testenv:lint]
|
||||
deps =
|
||||
black
|
||||
pylint
|
||||
shellcheck-py
|
||||
allowlist_externals = shfmt
|
||||
commands =
|
||||
black --check --diff --line-length 79 tests/
|
||||
pylint --rcfile=tests/.pylintrc tests/*.py
|
||||
shellcheck qm-device-manager/oci-qm-device-manager
|
||||
shellcheck wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shellcheck lib/common.sh
|
||||
shellcheck lib/device-support.sh
|
||||
shellcheck lib/mock-device-support.sh
|
||||
shfmt -d -i 4 qm-device-manager/oci-qm-device-manager
|
||||
shfmt -d -i 4 wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shfmt -d -i 4 lib/common.sh
|
||||
shfmt -d -i 4 lib/device-support.sh
|
||||
shfmt -d -i 4 lib/mock-device-support.sh
|
||||
|
||||
[testenv:format]
|
||||
deps = black
|
||||
allowlist_externals = shfmt
|
||||
commands =
|
||||
black --line-length 79 tests/
|
||||
shfmt -w -i 4 qm-device-manager/oci-qm-device-manager
|
||||
shfmt -w -i 4 wayland-client-devices/oci-qm-wayland-client-devices
|
||||
shfmt -w -i 4 lib/common.sh
|
||||
shfmt -w -i 4 lib/device-support.sh
|
||||
shfmt -w -i 4 lib/mock-device-support.sh
|
||||
|
||||
[flake8]
|
||||
max-line-length = 79
|
||||
extend-ignore = E203, W503
|
|
@ -0,0 +1,63 @@
|
|||
# Wayland-client-devices
|
||||
|
||||
The wayland-client-devices OCI hook enables containers to access GPU hardware acceleration devices for Wayland client
|
||||
applications that would run as qm's nested containers.
|
||||
|
||||
## Key Functionality
|
||||
|
||||
1. **GPU Device Discovery**: The hook automatically discovers and configures access to GPU render devices when
|
||||
GPU support is enabled, including:
|
||||
|
||||
- GPU hardware acceleration devices
|
||||
|
||||
2. **Container Configuration**: It dynamically modifies the container's OCI configuration to include the necessary
|
||||
GPU device permissions and access controls for Wayland client applications that require hardware acceleration.
|
||||
|
||||
3. **Comprehensive Logging**: All operations are logged to `/var/log/qm-wayland-client-devices.log` for monitoring and debugging.
|
||||
|
||||
## Configuration
|
||||
|
||||
The hook supports the following container annotation:
|
||||
|
||||
- `org.containers.qm.wayland-client.gpu`: Enables GPU device access for Wayland clients. When this annotation is
|
||||
present, the hook will automatically detect and configure access to available render devices in `/dev/dri/`.
|
||||
|
||||
## Logging
|
||||
|
||||
The hook provides detailed logging of all operations:
|
||||
|
||||
- **Log File**: `/var/log/qm-wayland-client-devices.log`
|
||||
- **Log Format**: `YYYY-MM-DD HH:MM:SS - qm-wayland-client-devices - LEVEL - MESSAGE`
|
||||
- **Log Levels**: INFO, WARNING, ERROR
|
||||
|
||||
### Example Log Output
|
||||
|
||||
```text
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Processing Wayland client GPU annotation: true
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Scanning for GPU render devices
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Adding GPU render device: /dev/dri/renderD128
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Found 1 GPU render devices
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Processing 1 GPU devices for Wayland client
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Added GPU device: /dev/dri/renderD128
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Successfully processed all GPU devices for Wayland client
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - Total devices in final spec: 1
|
||||
2024-01-15 10:32:15 - qm-wayland-client-devices - INFO - QM Wayland Client Devices hook completed successfully
|
||||
```
|
||||
|
||||
## Example Configuration
|
||||
|
||||
To use the Wayland-client-devices hook, you can create a dropin configuration file to add the necessary annotation
|
||||
to your container:
|
||||
|
||||
1. Create a dropin directory and file:
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/containers/systemd/myapp.container.d/
|
||||
```
|
||||
|
||||
2. Create a dropin file (e.g., `wayland-client.conf`):
|
||||
|
||||
```ini
|
||||
[Container]
|
||||
Annotation=org.containers.qm.wayland-client.gpu=true
|
||||
```
|
|
@ -0,0 +1,123 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Source common utilities and appropriate device support library
|
||||
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
# shellcheck source=../lib/common.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
if [[ -n "${TEST_LOGFILE:-}" ]]; then
|
||||
# Test mode - use mock device support
|
||||
# shellcheck source=../lib/mock-device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/mock-device-support.sh"
|
||||
else
|
||||
# Normal mode - use standard device support
|
||||
# shellcheck source=../lib/device-support.sh disable=SC1091
|
||||
source "${SCRIPT_DIR}/../lib/device-support.sh"
|
||||
fi
|
||||
|
||||
# Configuration
|
||||
LOGFILE="${TEST_LOGFILE:-/var/log/qm-wayland-client-devices.log}"
|
||||
# shellcheck disable=SC2034 # Used by log() function in device-support.sh
|
||||
HOOK_NAME="qm-wayland-client-devices"
|
||||
|
||||
input="-"
|
||||
CONTAINER_CONFIG=$(cat "$input")
|
||||
|
||||
if [[ -z "$CONTAINER_CONFIG" ]]; then
|
||||
log "ERROR" "Failed to read OCI spec from stdin"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GPU_ENABLED=$(echo "$CONTAINER_CONFIG" | jq -r '.annotations["org.containers.qm.wayland-client.gpu"] // empty')
|
||||
|
||||
DEVNAME_LIST=()
|
||||
|
||||
if [[ "$GPU_ENABLED" =~ ^(true|1|yes|True|TRUE|YES)$ ]]; then
|
||||
log "INFO" "Processing Wayland client GPU annotation: $GPU_ENABLED (enabled)"
|
||||
|
||||
# Find all the render devices available
|
||||
RENDER_DEVICES=$(discover_gpu_devices)
|
||||
log "INFO" "Scanning for GPU render devices"
|
||||
|
||||
for RENDER_DEVICE in $RENDER_DEVICES; do
|
||||
DEVNAME_LIST+=("$RENDER_DEVICE")
|
||||
log "INFO" "Adding GPU render device: $RENDER_DEVICE"
|
||||
done
|
||||
|
||||
log "INFO" "Found ${#DEVNAME_LIST[@]} GPU render devices"
|
||||
elif [ -n "$GPU_ENABLED" ]; then
|
||||
log "INFO" "Wayland client GPU annotation present but disabled: $GPU_ENABLED"
|
||||
else
|
||||
log "INFO" "No Wayland client GPU annotation found"
|
||||
fi
|
||||
|
||||
# Iterate over the DEVNAME_LIST to include the required information in the CONTAINER_CONFIG
|
||||
if [ ${#DEVNAME_LIST[@]} -gt 0 ]; then
|
||||
log "INFO" "Processing ${#DEVNAME_LIST[@]} GPU devices for Wayland client"
|
||||
|
||||
for DEVICE in "${DEVNAME_LIST[@]}"; do
|
||||
if ! jq -e ".linux.devices[] | select(.path == \"$DEVICE\")" <<<"$CONTAINER_CONFIG" >/dev/null 2>&1; then
|
||||
# Get device info using device support library
|
||||
if ! device_info=$(get_device_info "$DEVICE"); then
|
||||
log "WARNING" "Failed to get device info for $DEVICE, skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Parse device info: type:major:minor:file_mode:uid:gid
|
||||
IFS=':' read -r dev_type major minor filemode uid gid <<<"$device_info"
|
||||
|
||||
NEW_DEVICE=$(jq -n --arg path "$DEVICE" \
|
||||
--arg dev_type "$dev_type" \
|
||||
--arg major "$major" \
|
||||
--arg minor "$minor" \
|
||||
--arg filemode "$filemode" \
|
||||
--arg uid "$uid" \
|
||||
--arg gid "$gid" \
|
||||
'{
|
||||
"path": $path,
|
||||
"type": $dev_type,
|
||||
"major": $major|tonumber,
|
||||
"minor": $minor|tonumber,
|
||||
"fileMode": $filemode|tonumber,
|
||||
"uid": $uid|tonumber,
|
||||
"gid": $gid|tonumber,
|
||||
}')
|
||||
|
||||
NEW_DEV_RESOURCE=$(jq -n \
|
||||
--arg dev_type "$dev_type" \
|
||||
--arg major "$major" \
|
||||
--arg minor "$minor" \
|
||||
'{
|
||||
"allow": true,
|
||||
"type": $dev_type,
|
||||
"major": $major|tonumber,
|
||||
"minor": $minor|tonumber,
|
||||
"access": "rwm"
|
||||
}')
|
||||
|
||||
CONTAINER_CONFIG=$(jq ".linux.devices += [$NEW_DEVICE]" <<<"$CONTAINER_CONFIG")
|
||||
CONTAINER_CONFIG=$(jq ".linux.resources.devices += [$NEW_DEV_RESOURCE]" <<<"$CONTAINER_CONFIG")
|
||||
log "INFO" "Added GPU device: $DEVICE"
|
||||
else
|
||||
log "INFO" "GPU device already exists in spec: $DEVICE"
|
||||
fi
|
||||
done
|
||||
|
||||
log "INFO" "Successfully processed all GPU devices for Wayland client"
|
||||
else
|
||||
log "INFO" "No GPU devices to process for Wayland client"
|
||||
fi
|
||||
|
||||
# Initialize log file directory
|
||||
mkdir -p "$(dirname "$LOGFILE")"
|
||||
touch "$LOGFILE"
|
||||
|
||||
# Count total devices in final spec
|
||||
total_devices=$(echo "$CONTAINER_CONFIG" | jq '.linux.devices | length // 0')
|
||||
log "INFO" "Total devices in final spec: $total_devices"
|
||||
log "INFO" "QM Wayland Client Devices hook completed successfully"
|
||||
|
||||
echo "$CONTAINER_CONFIG" | jq .
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": "1.0.0",
|
||||
"hook": {
|
||||
"path": "/usr/libexec/oci/hooks.d/oci-qm-wayland-client-devices"
|
||||
},
|
||||
"when": {
|
||||
"annotations": {
|
||||
"^org\\.containers\\.qm\\.wayland-client\\..*$": "^.*$"
|
||||
}
|
||||
},
|
||||
"stages": ["precreate"]
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
summary: automotive-image-builder QM build test
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: 'tag:aib'
|
||||
|
||||
prepare:
|
||||
- name: Install packages
|
||||
how: install
|
||||
order: 20
|
||||
package:
|
||||
- jq
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
|
@ -0,0 +1,36 @@
|
|||
summary: FFI - QM FreedomFromInterference
|
||||
|
||||
environment+:
|
||||
CONTROL_CONTAINER_NAME: host
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: tag:ffi
|
||||
|
||||
provision:
|
||||
how: local
|
||||
|
||||
adjust:
|
||||
- when: distro == centos-stream-9, fedora
|
||||
environment+:
|
||||
FFI_SETUP_OPTIONS: --set-qm-disk-part=yes
|
||||
|
||||
- when: scenario == ffi
|
||||
prepare+:
|
||||
- name: Set QM env
|
||||
how: shell
|
||||
# FIXME: On new QM release remove
|
||||
# qm-setup-from-gh-url, branch-qm option
|
||||
script: |
|
||||
cd tests/e2e
|
||||
./set-ffi-env-e2e "${FFI_SETUP_OPTIONS}"
|
||||
- name: Place quadlet for ffi-qm container
|
||||
how: shell
|
||||
script: |
|
||||
cp tests/ffi/common/ffi-qm.container /etc/qm/containers/systemd/
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
|
@ -0,0 +1,18 @@
|
|||
summary: Kvm Tier 0 - QM sanity test
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: 'tier:0&tag:kvm|tier:0&tag:qmctl-test'
|
||||
|
||||
prepare+:
|
||||
- name: Enable copr and install rpms
|
||||
script: |
|
||||
cd tests/e2e
|
||||
bash ./lib/repoutils
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
summary: multiple bluechi-agents test - QM Interconnect through bluechi
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: tag:multi-bluechi-agents
|
||||
|
||||
provision:
|
||||
how: local
|
||||
|
||||
adjust:
|
||||
- when: distro == centos-stream-9 or distro == fedora
|
||||
prepare+:
|
||||
- name: Prepare Repos
|
||||
how: shell
|
||||
script: |
|
||||
dnf install -y dnf-plugin-config-manager epel-release
|
||||
dnf config-manager -y --set-enabled crb
|
||||
|
||||
- name: install repos
|
||||
how: install
|
||||
package:
|
||||
- podman
|
||||
|
||||
- name: Set QM env
|
||||
how: shell
|
||||
script: |
|
||||
cd tests/e2e
|
||||
./run-test-e2e --skip-tests=yes
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
|
@ -0,0 +1,19 @@
|
|||
summary: Tier 0 - QM sanity test
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: 'tier:0&tag:-setup'
|
||||
|
||||
prepare+:
|
||||
- name: Set QM environment
|
||||
how: shell
|
||||
order: 50
|
||||
script: |
|
||||
cd tests/e2e
|
||||
./set-ffi-env-e2e "${FFI_SETUP_OPTIONS}"
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
|
@ -0,0 +1,30 @@
|
|||
summary: Tier 1 - QM Interconnect through bluechi
|
||||
|
||||
discover:
|
||||
how: fmf
|
||||
filter: tier:1
|
||||
|
||||
provision:
|
||||
# Can not use provision no podman args
|
||||
how: local
|
||||
|
||||
adjust:
|
||||
prepare+:
|
||||
- name: Install rpms
|
||||
how: install
|
||||
package: podman
|
||||
|
||||
- name: Setup AutoSD env
|
||||
how: shell
|
||||
script: |
|
||||
# Currently option passing c9s
|
||||
podman run --replace -d --name autosd \
|
||||
--privileged \
|
||||
quay.io/centos-sig-automotive/autosd:latest
|
||||
when: distro == centos-stream-9 or distro == fedora
|
||||
|
||||
execute:
|
||||
how: tmt
|
||||
|
||||
report:
|
||||
how: junit
|
|
@ -0,0 +1,22 @@
|
|||
summary: general data used by the test plans
|
||||
|
||||
environment:
|
||||
FFI_SETUP_OPTIONS: none
|
||||
|
||||
prepare:
|
||||
- name: Install podman
|
||||
how: install
|
||||
order: 20
|
||||
package:
|
||||
- podman
|
||||
- bc
|
||||
|
||||
adjust:
|
||||
- when: run == manual
|
||||
environment+:
|
||||
# Sample code to use manual packit repo
|
||||
PACKIT_COPR_PROJECT: packit/containers-qm-291
|
||||
|
||||
- when: distro == centos-stream-9, fedora
|
||||
environment+:
|
||||
CS_DISTRO: $@distro
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
if command -v packit; then
|
||||
packit -d validate-config
|
||||
else
|
||||
echo "packit not installed, can't validate the config"
|
||||
echo "either install packit or try the validate-config-in-container hook"
|
||||
fi
|
80
qm.8.md
80
qm.8.md
|
@ -1,9 +1,11 @@
|
|||
% QM 8
|
||||
|
||||
## NAME
|
||||
|
||||
QM - Set up a Containerized environment for running Functional Safety QM (Quality Management) software.
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
This package allows users to set up an environment that prevents applications
|
||||
and container tools from interfering with other processes on the
|
||||
system.
|
||||
|
@ -22,12 +24,12 @@ use container tools like Podman.
|
|||
After the QM software package is installed, execute the
|
||||
/usr/share/qm/setup script to setup and install the /usr/lib/qm/rootfs
|
||||
with packages to run an isolated environment. The setup script installs the
|
||||
**selinux-policy-targeted**, **podman**, **systemd**, and **hirte** packages.
|
||||
**selinux-policy-targeted**, **podman**, **systemd**, and **bluechi** packages.
|
||||
Setup then enables and starts a Podman quadlet service qm.service (qm.container).
|
||||
|
||||
This Podman quadlet can be examined with the following command:
|
||||
|
||||
```
|
||||
```console
|
||||
systemctl status qm.service
|
||||
● qm.service
|
||||
Loaded: loaded (/etc/containers/systemd/qm.container; generated)
|
||||
|
@ -38,7 +40,7 @@ systemctl status qm.service
|
|||
Tasks: 11 (limit: 76801)
|
||||
Memory: 275.1M (swap max: 0B)
|
||||
CPU: 4.527s
|
||||
CGroup: /QM.slice/qm.service
|
||||
CGroup: /qm.service
|
||||
├─libpod-payload-00de006493bc970788d6c830beb494a58a9a2847a5eda200812d3a8b4e214814
|
||||
│ ├─init.scope
|
||||
│ │ └─993676 /sbin/init
|
||||
|
@ -51,18 +53,20 @@ systemctl status qm.service
|
|||
...
|
||||
```
|
||||
|
||||
## CGROUPS QM.slice
|
||||
## CGroups and container configuration
|
||||
|
||||
Notice that the QM environment is running systemd and other services within the
|
||||
QM.Slice. This slice can be used to modify the cgroups controls of all of the
|
||||
processes within the QM environment.
|
||||
The options in the qm.container file overridden by using drop-in files, in the
|
||||
directories `/etc/containers/systemd/qm.container.d` or`
|
||||
`/usr/lib/containers/systemd/qm.container.d. This allows overriding for example
|
||||
CGroup options like Service.CPUWeight, or podman options like Container.Volume.
|
||||
Such options will affect all the processes running in the qm container.
|
||||
|
||||
## Install Additional packages in QM
|
||||
|
||||
If other packages need to be added into the QM environment, use the `dnf` command
|
||||
on the host. For example, the following example installs the dnf command into the QM environment:
|
||||
|
||||
```
|
||||
```console
|
||||
# dnf install --installroot=/usr/lib/qm/rootfs dnf
|
||||
Unable to read consumer identity
|
||||
|
||||
|
@ -78,16 +82,17 @@ Installing:
|
|||
|
||||
## Entering the QM
|
||||
|
||||
To enter the QM environment, use this Podman command to
|
||||
To enter the QM environment, use this Podman command to
|
||||
launch containers within it.
|
||||
|
||||
```
|
||||
```console
|
||||
sh-5.2# podman exec -ti qm sh
|
||||
sh-5.2#
|
||||
```
|
||||
|
||||
The SELinux label can be checked by executing the following:
|
||||
|
||||
```
|
||||
```console
|
||||
sh-5.2# id -Z
|
||||
system_u:system_r:qm_t:s0:c35,c404
|
||||
```
|
||||
|
@ -97,14 +102,14 @@ confined QM process within the QM environment.
|
|||
|
||||
Containers can now be run within the QM environment using Podman.
|
||||
|
||||
```
|
||||
```console
|
||||
sh-5.2# podman run --rm ubi9-minimal echo hi
|
||||
Resolved "ubi9-minimal" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
|
||||
Trying to pull registry.access.redhat.com/ubi9-minimal:latest...
|
||||
Getting image source signatures
|
||||
Checking if image destination supports signatures
|
||||
Copying blob 7bffb309b4e8 done
|
||||
Copying config 96179718b4 done
|
||||
Copying blob 7bffb309b4e8 done
|
||||
Copying config 96179718b4 done
|
||||
Writing manifest to image destination
|
||||
Storing signatures
|
||||
hi
|
||||
|
@ -115,5 +120,50 @@ applications within the QM. All applications within the QM environment are
|
|||
prevented from interfering with applications running outside of the QM
|
||||
environment.
|
||||
|
||||
## Configuring bluechi agent in the QM
|
||||
|
||||
The configuration of the hosts /etc/bluechi/agent.conf file is copied into the QM every time the
|
||||
qm.service is started, with the nodename of the hosts agent.conf modified by prepending `qm.`
|
||||
on the front of the nodename. If the hosts /etc/bluechi/agent.conf does not exists, then the
|
||||
QM bluechi agent will default to `qm.`$(hostname).
|
||||
|
||||
If you want permanently modify the bluechi agent within the QM you can add config to
|
||||
/usr/lib/qm/rootfs/etc/bluechi/agent.conf.d/ directory or modify the /etc/containers/systemd/qm.container
|
||||
quadlet file to not execute the bluechi-agent setup script.
|
||||
|
||||
## Using systemd drop-in with QM
|
||||
|
||||
The systemd drop-in feature allows users to extend or modify the configuration of systemd units without directly editing the unit files provided by the system package.
|
||||
|
||||
In this example, we will add the AllowedCPU to set as 1 to qm.service that quadlet generated:
|
||||
|
||||
### Create allowedcpus.conf
|
||||
|
||||
```console
|
||||
cat /etc/systemd/system/qm.service.d/allowedcpus.conf
|
||||
|
||||
# Contents of qm.continer.d/allowedcpus.conf
|
||||
[Service]
|
||||
AllowedCPUs=1
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### qm-rootfs
|
||||
|
||||
Prints the qm rootfs location previously configured during setup.
|
||||
|
||||
### qm-storage-settings
|
||||
|
||||
Setup the initial QM configuration for storage using the follow config files and changes:
|
||||
|
||||
- `${ROOTFS}/etc/containers/storage.conf`
|
||||
- uncomment `additionalimagestores`
|
||||
- add `/var/lib/shared` into `additionalimagestores`
|
||||
- uncomment and set to `true` the option `transient_store`
|
||||
- `${ROOTFS}/etc/containers/container.conf`
|
||||
- add `[engine]` and `TMPDIR`
|
||||
|
||||
## SEE ALSO
|
||||
**[podman(1)](https://github.com/containers/podman/blob/main/docs/source/markdown/podman.1.md)**, **[quadlet(5)](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-systemd.unit.5.md)**, systemctl(1), systemd(1), dnf(8)
|
||||
|
||||
**[podman(1)](https://github.com/containers/podman/blob/main/docs/source/markdown/podman.1.md)**,**[quadlet(5)](https://github.com/containers/podman/blob/main/docs/source/markdown/podman-systemd.unit.5.md)**, systemctl(1), systemd(1), dnf(8), [bluechi-agent(1)](https://github.com/containers/bluechi/blob/main/doc/man/bluechi-agent.1.md),[bluechi-agent.conf.5](https://github.com/containers/bluechi/blob/main/doc/man/bluechi-agent.conf.5.md)
|
||||
|
|
106
qm.container
106
qm.container
|
@ -1,26 +1,112 @@
|
|||
[Install]
|
||||
WantedBy=default.target
|
||||
|
||||
[Service]
|
||||
AllowedCPUs=6-11
|
||||
CPUWeight=50
|
||||
# It's recommended to use systemd drop-in to override the
|
||||
# systemd settings. See QM manpage for an example.
|
||||
# CPUWeight: This setting controls the CPU controller for qm.
|
||||
# Delegate: Turns on delegation of further resource control
|
||||
# partitioning to processes of the unit.
|
||||
# IOWeight: Set the overall block I/O weight for qm.
|
||||
# ManagedOOMSwap=auto|kill: Specifies how systemd-oomd.service will act on qm.
|
||||
# QM cgroup, pass directly to systemd and handled by it,
|
||||
# please refer to `man systemd.resource-control` for details.
|
||||
CPUWeight=idle
|
||||
Delegate=true
|
||||
IOWeight=50
|
||||
ManagedOOMSwap=kill
|
||||
|
||||
# MemoryMax
|
||||
# -----------
|
||||
# Default is infinity - sets no upper bound on memory usage, allowing the
|
||||
# service to consume as much memory as the system allows, just like
|
||||
# MemoryMax=0 or omitting it.
|
||||
MemoryMax=infinity
|
||||
|
||||
# MemoryHigh
|
||||
# -----------
|
||||
# Default is infinity - Disables the soft memory limit, meaning no throttling
|
||||
# will occur due to high memory usage, similar to MemoryHigh=0 or
|
||||
# omitting it.
|
||||
MemoryHigh=infinity
|
||||
MemorySwapMax=0
|
||||
# Containers within the qm y default set OOMScoreAdj to 750
|
||||
OOMScoreAdjust=500
|
||||
|
||||
# Containers within the qm contain default set OOMScoreAdj to 750
|
||||
OOMScoreAdjust=500
|
||||
Restart=always
|
||||
Slice=QM.slice
|
||||
# qm.service is a toplevel cgroup, so CPUWeight is relative to all other cgroups in that
|
||||
# parent (such as user.slice and system.slice), otherwise the CPUWeight of qm.service
|
||||
# is only compared to the other children of its parent.
|
||||
Slice=-.slice
|
||||
Environment=ROOTFS=/usr/lib/qm/rootfs
|
||||
Environment=RWETCFS=/etc/qm
|
||||
Environment=RWVARFS=/var/qm
|
||||
LimitNOFILE=65536
|
||||
TasksMax=50%
|
||||
|
||||
[Container]
|
||||
# AddCapability
|
||||
# -------------
|
||||
# Add these capabilities, in addition to the default Podman capability set, to the container.
|
||||
# If set to all, grants all capabilities to the container, increasing flexibility but significantly
|
||||
# reducing security.
|
||||
# For details see: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#addcapability
|
||||
AddCapability=all
|
||||
|
||||
SecurityLabelNested=true
|
||||
SeccompProfile=/usr/share/qm/seccomp-no-rt.json
|
||||
|
||||
# PidsLimit
|
||||
# ---------
|
||||
# Disables the PID limit for the container by setting it to -1.
|
||||
# Without a limit, the container can spawn unlimited processes, potentially exhausting system resources.
|
||||
# For details see: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#pidslimit
|
||||
PidsLimit=-1
|
||||
|
||||
# Comment DropCapability this will allow FFI Tools to surpass their defaults.
|
||||
DropCapability=sys_boot sys_resource
|
||||
|
||||
AddDevice=-/dev/kvm
|
||||
AddDevice=-/dev/fuse
|
||||
ContainerName=qm
|
||||
Exec=/sbin/init
|
||||
Network=host
|
||||
PodmanArgs=--security-opt label=nested --security-opt unmask=all
|
||||
Network=private
|
||||
|
||||
# ReadOnly
|
||||
# --------
|
||||
# Makes the container's filesystem read-only, enhancing security by preventing modifications.
|
||||
ReadOnly=true
|
||||
Rootfs=/usr/lib/qm/rootfs
|
||||
|
||||
# TmpFS flags
|
||||
ReadOnlyTmpfs=false
|
||||
Mount=type=tmpfs,tmpfs-size=512M,destination=/tmp
|
||||
Mount=type=tmpfs,tmpfs-size=512M,destination=/run
|
||||
Mount=type=tmpfs,destination=/dev/shm
|
||||
|
||||
# Rootfs
|
||||
# ------
|
||||
# Defines the root filesystem location for QM partition.
|
||||
# By default the '${ROOTFS}' variable points to /usr/lib/qm/rootfs.
|
||||
# For details see: https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html#rootfs
|
||||
#
|
||||
Rootfs=${ROOTFS}
|
||||
|
||||
# The value for fs.mqueue.queues_max determines the maximum number of POSIX message queues
|
||||
# that can be created system-wide within the container. This value is set based on available
|
||||
# system resources and the expected usage pattern. The calculation is approximately:
|
||||
# queues_max ≈ (system-wide RLIMIT_MSGQUEUE) / (msg_max * msgsize_max) / 2
|
||||
# where:
|
||||
# - RLIMIT_MSGQUEUE is the total bytes allowed for all message queues (ulimit -q)
|
||||
# - msg_max is the maximum number of messages allowed per queue (msg_max)
|
||||
# - msgsize_max is the maximum size of each message (msgsize_max)
|
||||
# The division by 2 provides a safety margin to avoid exhausting resources and grant FFI.
|
||||
Sysctl=fs.mqueue.queues_max=4
|
||||
|
||||
SecurityLabelNested=true
|
||||
SecurityLabelFileType=qm_file_t
|
||||
SecurityLabelLevel=s0
|
||||
SecurityLabelType=qm_t
|
||||
Volume=qmEtc:/etc:copy
|
||||
Volume=qmVar:/var:copy
|
||||
Timezone=local
|
||||
Volume=${RWETCFS}:/etc
|
||||
Volume=${RWVARFS}:/var
|
||||
|
||||
|
|
9
qm.fc
9
qm.fc
|
@ -6,3 +6,12 @@
|
|||
/usr/lib/qm/rootfs/var/lib/containers/storage/overlay2-layers(/.*)? gen_context(system_u:object_r:qm_container_ro_file_t,s0)
|
||||
/usr/lib/qm/rootfs/var/lib/containers/storage/overlay-images(/.*)? gen_context(system_u:object_r:qm_container_ro_file_t,s0)
|
||||
/usr/lib/qm/rootfs/var/lib/containers/storage/overlay2-images(/.*)? gen_context(system_u:object_r:qm_container_ro_file_t,s0)
|
||||
/var/qm(/.*)? gen_context(system_u:object_r:qm_file_t,s0)
|
||||
/etc/qm(/.*)? gen_context(system_u:object_r:qm_file_t,s0)
|
||||
|
||||
# File context for ipc programs
|
||||
/usr/lib/qm/rootfs/run/ipc(/.*)? gen_context(system_u:object_r:ipc_var_run_t,s0)
|
||||
/run/ipc(/.*)? gen_context(system_u:object_r:ipc_var_run_t,s0)
|
||||
|
||||
# File context for bluechi-agent inside QM
|
||||
/usr/lib/qm/rootfs/usr/libexec/bluechi-agent -- gen_context(system_u:object_r:qm_bluechi_agent_exec_t,s0)
|
||||
|
|
245
qm.if
245
qm.if
|
@ -15,14 +15,15 @@ template(`qm_domain_template',`
|
|||
gen_require(`
|
||||
class dbus { send_msg acquire_svc };
|
||||
class passwd rootok;
|
||||
class process setcurrent;
|
||||
|
||||
attribute container_domain;
|
||||
attribute filesystem_type;
|
||||
attribute container_init_domain;
|
||||
attribute container_net_domain;
|
||||
attribute container_user_domain;
|
||||
attribute unconfined_domain_type;
|
||||
|
||||
type ipc_t;
|
||||
type ipc_var_run_t;
|
||||
type cgroup_t;
|
||||
type container_runtime_t;
|
||||
type devpts_t;
|
||||
|
@ -37,19 +38,28 @@ template(`qm_domain_template',`
|
|||
type sysctl_irq_t;
|
||||
type sysctl_t;
|
||||
type system_dbusd_t;
|
||||
type systemd_hostnamed_t;
|
||||
type systemd_logind_t;
|
||||
type systemd_machined_t;
|
||||
type unconfined_service_t;
|
||||
type bpf_t;
|
||||
type container_devpts_t;
|
||||
type net_conf_t;
|
||||
type getty_t;
|
||||
')
|
||||
|
||||
type $1_t;
|
||||
domain_type($1_t)
|
||||
role system_r types $1_t;
|
||||
unconfined_stub_role()
|
||||
role unconfined_r types $1_t;
|
||||
init_initrc_domain($1_t)
|
||||
container_use_ptys($1_t)
|
||||
container_read_share_files($1_t)
|
||||
container_exec_share_files($1_t)
|
||||
allow $1_t container_ro_file_t:file execmod;
|
||||
allow $1_container_domain $1_file_type:chr_file { rw_inherited_file_perms };
|
||||
allow $1_t self:process setcurrent;
|
||||
|
||||
attribute $1_file_type;
|
||||
allow $1_file_type self:filesystem associate;
|
||||
|
@ -71,6 +81,12 @@ template(`qm_domain_template',`
|
|||
manage_files_pattern($1_t, $1_file_type, $1_file_type)
|
||||
can_exec($1_t, $1_file_type)
|
||||
allow $1_t $1_file_type:chr_file mounton;
|
||||
allow $1_t $1_file_type:sock_file mounton;
|
||||
|
||||
filetrans_pattern(ipc_t, $1_file_t, ipc_var_run_t, dir, "ipc")
|
||||
list_dirs_pattern($1_t, ipc_var_run_t, ipc_var_run_t)
|
||||
allow $1_t ipc_var_run_t:dir mounton;
|
||||
|
||||
manage_blk_files_pattern($1_t, $1_file_type, $1_file_type)
|
||||
manage_chr_files_pattern($1_t, $1_file_type, $1_file_type)
|
||||
manage_dirs_pattern($1_t, $1_file_type, $1_file_type)
|
||||
|
@ -78,12 +94,17 @@ template(`qm_domain_template',`
|
|||
manage_lnk_files_pattern($1_t, $1_file_type, $1_file_type)
|
||||
manage_sock_files_pattern($1_t, $1_file_type, $1_file_type)
|
||||
fs_tmpfs_filetrans($1_t, $1_file_t, { dir file lnk_file })
|
||||
allow $1_t $1_file_type:chr_file { watch watch_reads };
|
||||
allow $1_t $1_file_type:chr_file { watch watch_reads map };
|
||||
allow $1_t $1_file_type:dir { mounton relabelfrom relabelto };
|
||||
allow $1_t $1_file_type:filesystem all_filesystem_perms;
|
||||
|
||||
allow $1_t $1_file_type:service all_service_perms;
|
||||
|
||||
container_read_share_files($1_container_domain)
|
||||
container_exec_share_files($1_container_domain)
|
||||
allow $1_container_domain container_ro_file_t:file execmod;
|
||||
|
||||
allow init_t $1_file_type:file read_file_perms;
|
||||
manage_blk_files_pattern(init_t, $1_file_type, $1_file_type)
|
||||
manage_chr_files_pattern(init_t, $1_file_type, $1_file_type)
|
||||
manage_dirs_pattern(init_t, $1_file_type, $1_file_type)
|
||||
|
@ -98,8 +119,8 @@ template(`qm_domain_template',`
|
|||
filetrans_pattern($1_t, $1_container_var_lib_t, $1_container_ro_file_t, dir, "overlay2")
|
||||
filetrans_pattern($1_t, $1_container_var_lib_t, $1_container_ro_file_t, dir, "overlay2-imagess")
|
||||
filetrans_pattern($1_t, $1_container_var_lib_t, $1_container_ro_file_t, dir, "overlay2-layers")
|
||||
allow $1_container_domain $1_container_ro_file_t:file execmod;
|
||||
|
||||
allow container_domain $1_container_ro_file_t:file execmod;
|
||||
ps_process_pattern(systemd_machined_t, $1_t)
|
||||
read_files_pattern(systemd_machined_t, $1_file_type, $1_file_type)
|
||||
list_dirs_pattern(systemd_machined_t, $1_file_type, $1_file_type)
|
||||
|
@ -107,8 +128,8 @@ template(`qm_domain_template',`
|
|||
rw_sock_files_pattern(systemd_machined_t, $1_file_type, $1_file_type)
|
||||
manage_chr_files_pattern(systemd_machined_t, $1_file_type, $1_file_type)
|
||||
allow systemd_machined_t $1_t:unix_stream_socket { connectto rw_stream_socket_perms };
|
||||
allow system_dbusd_t $1_file_type:chr_file { read write };
|
||||
allow systemd_machined_t unconfined_service_t:dir search;
|
||||
allow system_dbusd_t $1_file_type:chr_file rw_chr_file_perms;
|
||||
allow systemd_machined_t unconfined_service_t:dir search_dir_perms;
|
||||
systemd_dbus_chat_machined($1_t)
|
||||
allow systemd_machined_t self:cap_userns kill;
|
||||
|
||||
|
@ -120,13 +141,13 @@ template(`qm_domain_template',`
|
|||
manage_chr_files_pattern(systemd_logind_t, $1_file_type, $1_file_type)
|
||||
allow systemd_logind_t $1_t:unix_stream_socket { connectto rw_stream_socket_perms };
|
||||
|
||||
allow system_dbusd_t $1_file_type:chr_file { read write };
|
||||
allow system_dbusd_t $1_file_type:chr_file rw_chr_file_perms;
|
||||
|
||||
allow $1_t self:system all_system_perms;
|
||||
allow $1_t self:user_namespace all_user_namespace_perms;
|
||||
allow $1_t self:bpf { map_create map_read map_write prog_load prog_run };
|
||||
allow $1_t self:cap_userns { audit_write chown dac_override dac_read_search fowner fsetid kill net_bind_service net_admin net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_ptrace sys_resource };
|
||||
allow $1_t self:capability { audit_write chown dac_override dac_read_search fowner fsetid kill net_bind_service net_admin net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_ptrace sys_resource };
|
||||
allow $1_t self:capability { audit_write chown dac_override dac_read_search fowner fsetid ipc_lock kill net_bind_service net_admin net_raw setfcap setgid setpcap setuid sys_admin sys_boot sys_chroot sys_nice sys_ptrace sys_resource sys_tty_config };
|
||||
allow $1_t self:capability2 { audit_read bpf perfmon};
|
||||
|
||||
allow $1_t self:packet_socket create_socket_perms;
|
||||
|
@ -154,15 +175,24 @@ template(`qm_domain_template',`
|
|||
|
||||
seutil_search_default_contexts($1_t)
|
||||
|
||||
allow $1_t bpf_t:dir mounton;
|
||||
allow $1_t cgroup_t:filesystem { getattr remount};
|
||||
allow $1_t cgroup_t:{dir file } mounton;
|
||||
allow $1_t container_devpts_t:chr_file { watch watch_reads };
|
||||
allow $1_t container_runtime_t:fifo_file rw_fifo_file_perms;
|
||||
allow $1_t devpts_t:filesystem relabelfrom;
|
||||
allow $1_t hugetlbfs_t:dir relabelfrom;
|
||||
allow $1_t mtrr_device_t:file { getattr mounton };
|
||||
allow $1_t proc_kcore_t:file { getattr mounton };
|
||||
allow $1_t proc_kmsg_t:file { getattr mounton };
|
||||
allow $1_t proc_t:file mounton;
|
||||
allow $1_t security_t:dir read;
|
||||
allow $1_t sysctl_irq_t:dir { getattr mounton };
|
||||
allow $1_t sysctl_t:file { getattr mounton };
|
||||
allow $1_t cgroup_t:filesystem { getattr remount };
|
||||
allow $1_t container_devpts_t:chr_file { watch watch_reads };
|
||||
allow $1_t devpts_t:filesystem relabelfrom;
|
||||
|
||||
corecmd_entrypoint_all_executables($1_t)
|
||||
corecmd_exec_bin($1_t)
|
||||
corecmd_exec_shell($1_t)
|
||||
|
||||
corenet_icmp_bind_generic_node($1_t)
|
||||
corenet_raw_bind_generic_node($1_t)
|
||||
|
@ -177,13 +207,17 @@ template(`qm_domain_template',`
|
|||
corenet_udp_bind_generic_node($1_t)
|
||||
corenet_udp_sendrecv_all_ports($1_t)
|
||||
|
||||
dev_dontaudit_mounton_sysfs($1_container_domain)
|
||||
dev_getattr_mtrr_dev($1_container_domain)
|
||||
dev_list_sysfs($1_container_domain)
|
||||
dev_list_sysfs($1_t)
|
||||
dev_mounton_sysfs($1_t)
|
||||
dev_mounton_sysfs($1_t)
|
||||
dev_read_rand($1_t)
|
||||
dev_read_sysfs($1_t)
|
||||
dev_read_urand($1_t)
|
||||
dev_remount_sysfs_fs($1_t)
|
||||
allow $1_t bpf_t:dir mounton;
|
||||
allow $1_t container_runtime_t:fifo_file write;
|
||||
dev_write_sysfs_dirs($1_t)
|
||||
|
||||
files_getattr_all_blk_files($1_t)
|
||||
files_getattr_all_chr_files($1_t)
|
||||
|
@ -206,8 +240,12 @@ template(`qm_domain_template',`
|
|||
fs_relabelfrom_tmpfs($1_t)
|
||||
fs_relabelfrom_xattr_fs($1_t)
|
||||
fs_search_tracefs_dirs($1_t)
|
||||
fs_set_xattr_fs_quotas($1_t)
|
||||
allow $1_t nsfs_t:filesystem { getattr unmount };
|
||||
|
||||
domain_obj_id_change_exemption($1_t)
|
||||
|
||||
kernel_dgram_send($1_t)
|
||||
kernel_dontaudit_search_security_state($1_t)
|
||||
kernel_list_all_proc($1_t)
|
||||
kernel_mounton_core_if($1_t)
|
||||
|
@ -232,17 +270,25 @@ template(`qm_domain_template',`
|
|||
kernel_rw_net_sysctls($1_t)
|
||||
kernel_rw_security_state($1_t)
|
||||
kernel_rw_unix_sysctls($1_t)
|
||||
kernel_rw_vm_sysctls($1_t)
|
||||
kernel_rw_usermodehelper_state($1_t)
|
||||
kernel_rw_vm_sysctls($1_t)
|
||||
kernel_search_debugfs($1_t)
|
||||
dontaudit $1_t proc_security_t:file write;
|
||||
allow $1_t filesystem_type:filesystem { mount remount unmount };
|
||||
kernel_search_debugfs($1_t)
|
||||
|
||||
unconfined_dgram_send($1_t)
|
||||
|
||||
selinux_compute_access_vector($1_t)
|
||||
selinux_compute_create_context($1_t)
|
||||
selinux_dontaudit_get_fs_mount($1_t)
|
||||
selinux_dontaudit_search_fs($1_t)
|
||||
selinux_get_enforce_mode($1_t)
|
||||
selinux_mounton_fs($1_t)
|
||||
selinux_setcheckreqprot($1_t)
|
||||
selinux_validate_context($1_t)
|
||||
dontaudit $1_t security_t:file write;
|
||||
dontaudit $1_t security_t:security read_policy;
|
||||
|
||||
sysnet_read_config($1_t)
|
||||
sysnet_write_config($1_t)
|
||||
|
@ -251,16 +297,7 @@ template(`qm_domain_template',`
|
|||
term_use_generic_ptys($1_t)
|
||||
term_setattr_generic_ptys($1_t)
|
||||
|
||||
dev_write_sysfs_dirs($1_t)
|
||||
|
||||
allow $1_t security_t:dir read;
|
||||
allow $1_t hugetlbfs_t:dir relabelfrom;
|
||||
|
||||
dontaudit $1_t security_t:security read_policy;
|
||||
selinux_compute_access_vector($1_t)
|
||||
selinux_compute_create_context($1_t)
|
||||
selinux_get_enforce_mode($1_t)
|
||||
selinux_validate_context($1_t)
|
||||
userdom_use_inherited_user_ptys($1_t)
|
||||
|
||||
allow container_runtime_t $1_t:process { dyntransition transition };
|
||||
allow $1_t container_runtime_t:process sigchld;
|
||||
|
@ -290,18 +327,26 @@ template(`qm_domain_template',`
|
|||
domain_user_exemption_target($1_container_t)
|
||||
container_manage_files_template($1_container, $1_container)
|
||||
|
||||
type $1_container_ipc_t, $1_container_domain;
|
||||
domain_type($1_container_ipc_t)
|
||||
domain_user_exemption_target($1_container_ipc_t)
|
||||
container_manage_files_template($1_container_ipc, $1_container)
|
||||
container_ipc_stream_connect($1_container_ipc_t)
|
||||
container_stream_connect($1_container_ipc_t)
|
||||
|
||||
type $1_container_file_t, $1_file_type;
|
||||
files_type($1_container_file_t)
|
||||
files_mountpoint($1_container_file_t)
|
||||
fs_associate($1_container_file_t)
|
||||
allow $1_container_domain $1_file_type:file { execmod relabelfrom relabelto map entrypoint mounton };
|
||||
allow $1_container_domain $1_file_type:file { execmod relabelfrom relabelto map entrypoint mounton};
|
||||
allow $1_container_domain $1_file_type:dir search_dir_perms;
|
||||
exec_files_pattern($1_container_domain, $1_file_type, $1_file_type)
|
||||
list_dirs_pattern($1_container_domain, $1_file_type, $1_file_type)
|
||||
read_files_pattern($1_container_domain, $1_file_type, $1_file_type)
|
||||
|
||||
# QM Container kvm - Policy for running kata containers
|
||||
type $1_container_kvm_t, $1_container_domain;
|
||||
domain_type($1_container_kvm_t)
|
||||
domain_user_exemption_target($1_container_kvm_t)
|
||||
typeattribute $1_container_kvm_t container_net_domain, container_user_domain;
|
||||
container_manage_files_template($1_container_kvm, $1_container)
|
||||
filetrans_pattern($1_container_t, $1_file_t, $1_container_file_t, sock_file)
|
||||
|
||||
qm_container_template($1, kvm)
|
||||
|
||||
type $1_container_kvm_var_run_t;
|
||||
files_pid_file($1_container_kvm_var_run_t)
|
||||
|
@ -317,12 +362,16 @@ template(`qm_domain_template',`
|
|||
files_pid_filetrans($1_container_kvm_t, $1_container_kvm_var_run_t, { dir file lnk_file sock_file })
|
||||
allow $1_container_kvm_t $1_container_kvm_var_run_t:{file dir} mounton;
|
||||
|
||||
allow $1_container_kvm_t $1_t:unix_stream_socket rw_stream_socket_perms;
|
||||
manage_files_pattern($1_container_kvm_t, $1_file_t, $1_file_t)
|
||||
manage_sock_files_pattern($1_container_kvm_t, $1_file_t, $1_file_t)
|
||||
|
||||
allow $1_container_kvm_t $1_container_wayland_t:unix_stream_socket rw_stream_socket_perms;
|
||||
allow $1_container_kvm_t $1_t:unix_stream_socket { connectto rw_stream_socket_perms };
|
||||
container_stream_connect($1_container_kvm_t)
|
||||
|
||||
allow $1_container_kvm_t $1_t:tun_socket attach_queue;
|
||||
|
||||
dev_read_sysfs($1_container_kvm_t)
|
||||
dev_rw_inherited_vhost($1_container_kvm_t)
|
||||
dev_rw_vfio_dev($1_container_kvm_t)
|
||||
|
||||
|
@ -352,32 +401,29 @@ template(`qm_domain_template',`
|
|||
|
||||
sssd_read_public_files($1_container_kvm_t)
|
||||
|
||||
# Container init - Policy for running systemd based containers
|
||||
type $1_container_init_t, $1_container_domain;
|
||||
domain_type($1_container_init_t)
|
||||
domain_user_exemption_target($1_container_init_t)
|
||||
typeattribute $1_container_init_t container_init_domain, container_net_domain, container_user_domain;
|
||||
|
||||
corenet_unconfined($1_container_init_t)
|
||||
qm_container_template($1, init)
|
||||
logging_send_syslog_msg($1_container_init_t)
|
||||
|
||||
allow $1_container_init_t proc_t:filesystem remount;
|
||||
qm_container_template($1, wayland)
|
||||
|
||||
optional_policy(`
|
||||
virt_default_capabilities($1_container_init_t)
|
||||
')
|
||||
allow $1_container_wayland_t $1_file_t:chr_file map;
|
||||
manage_dirs_pattern($1_container_wayland_t, $1_file_t, $1_file_t)
|
||||
manage_files_pattern($1_container_wayland_t, $1_file_t, $1_file_t)
|
||||
manage_sock_files_pattern($1_container_wayland_t, $1_file_t, $1_file_t)
|
||||
allow $1_container_wayland_t $1_t:unix_stream_socket connectto;
|
||||
allow $1_container_wayland_t $1_t:dbus send_msg;
|
||||
allow $1_t $1_container_wayland_t:dbus send_msg;
|
||||
dev_read_sysfs($1_container_wayland_t)
|
||||
|
||||
tunable_policy(`virt_sandbox_use_sys_admin',`
|
||||
allow $1_container_init_t self:capability sys_admin;
|
||||
allow $1_container_init_t self:cap_userns sys_admin;
|
||||
')
|
||||
allow getty_t $1_file_type:chr_file rw_chr_file_perms;
|
||||
|
||||
allow $1_container_init_t self:netlink_audit_socket nlmsg_relay;
|
||||
container_manage_files_template($1_container_init, $1_container)
|
||||
read_files_pattern(systemd_hostnamed_t, $1_file_t, $1_file_t)
|
||||
systemd_dbus_chat_hostnamed(systemd_hostnamed_t)
|
||||
|
||||
read_files_pattern($1_container_t, $1_container_ro_file_t,$1_container_ro_file_t,)
|
||||
read_lnk_files_pattern($1_container_t, $1_container_ro_file_t,$1_container_ro_file_t,)
|
||||
list_dirs_pattern($1_container_t, $1_container_ro_file_t,$1_container_ro_file_t,)
|
||||
read_files_pattern($1_container_domain, $1_container_ro_file_t,$1_container_ro_file_t)
|
||||
read_lnk_files_pattern($1_container_domain, $1_container_ro_file_t,$1_container_ro_file_t)
|
||||
list_dirs_pattern($1_container_domain, $1_container_ro_file_t,$1_container_ro_file_t)
|
||||
|
||||
#
|
||||
# Rules for container domains in the qm
|
||||
|
@ -406,7 +452,7 @@ template(`qm_domain_template',`
|
|||
allow $1_container_domain self:packet_socket create_socket_perms;
|
||||
allow $1_container_domain self:passwd rootok;
|
||||
allow $1_container_domain self:peer recv;
|
||||
allow $1_container_domain self:process all_process_perms;
|
||||
allow $1_container_domain self:process { execmem execstack fork getattr getcap getpgid getsched getsession setcap setpgid setrlimit setsched sigchld sigkill signal signull sigstop};
|
||||
allow $1_container_domain self:sem create_sem_perms;
|
||||
allow $1_container_domain self:shm create_shm_perms;
|
||||
allow $1_container_domain self:socket_class_set { create_socket_perms map accept };
|
||||
|
@ -423,6 +469,7 @@ template(`qm_domain_template',`
|
|||
allow $1_t $1_container_domain:process { dyntransition transition };
|
||||
allow $1_t $1_container_domain:process2 { nnp_transition nosuid_transition };
|
||||
allow $1_t $1_container_domain:tun_socket relabelfrom;
|
||||
allow $1_container_domain $1_t:unix_dgram_socket sendto;
|
||||
|
||||
allow $1_container_domain container_runtime_t:unix_dgram_socket sendto;
|
||||
allow $1_container_domain container_runtime_tmpfs_t:dir mounton;
|
||||
|
@ -439,11 +486,14 @@ template(`qm_domain_template',`
|
|||
allow unconfined_domain_type $1_container_domain:process2 { nnp_transition nosuid_transition };
|
||||
allow unconfined_service_t $1_container_domain:process dyntransition;
|
||||
|
||||
dev_getattr_all($1_container_domain)
|
||||
dev_list_sysfs($1_container_domain)
|
||||
dev_dontaudit_mounton_sysfs($1_container_domain)
|
||||
domain_dontaudit_link_all_domains_keyrings($1_container_domain)
|
||||
domain_dontaudit_search_all_domains_keyrings($1_container_domain)
|
||||
domain_dontaudit_search_all_domains_state($1_container_domain)
|
||||
dontaudit $1_container_domain container_runtime_tmpfs_t:dir read;
|
||||
dontaudit $1_container_domain $1_t:chr_file getattr;
|
||||
dontaudit $1_container_domain $1_container_domain:key search;
|
||||
dontaudit $1_container_domain self:capability fsetid;
|
||||
dontaudit $1_container_domain self:capability2 block_suspend ;
|
||||
|
@ -526,4 +576,95 @@ template(`qm_domain_template',`
|
|||
|
||||
userdom_rw_inherited_user_pipes($1_container_domain)
|
||||
userdom_use_user_ptys($1_container_domain)
|
||||
|
||||
optional_policy(`
|
||||
vsomeip_use($1_t)
|
||||
vsomeip_use($1_container_domain)
|
||||
')
|
||||
')
|
||||
|
||||
########################################
|
||||
## <summary>
|
||||
## Creates types and rules for a basic
|
||||
## container runtime process domain.
|
||||
## </summary>
|
||||
## <param name="prefix">
|
||||
## <summary>
|
||||
## Prefix for the domain.
|
||||
## </summary>
|
||||
## </param>
|
||||
#
|
||||
interface(`vsomeip_use',`
|
||||
gen_require(`
|
||||
type vsomeip_t;
|
||||
type vsomeip_var_run_t;
|
||||
type router_vsomeip_var_run_t;
|
||||
')
|
||||
# create and use vsomeip sockets:
|
||||
allow $1 vsomeip_var_run_t:dir { add_name remove_name write };
|
||||
allow $1 vsomeip_var_run_t:sock_file { create setattr write unlink };
|
||||
|
||||
# Talk to routing manager (and back)
|
||||
allow $1 vsomeip_t:unix_stream_socket connectto;
|
||||
allow vsomeip_t $1:unix_stream_socket connectto;
|
||||
allow $1 router_vsomeip_var_run_t:sock_file write;
|
||||
')
|
||||
|
||||
########################################
|
||||
## <summary>
|
||||
## Creates types and rules for QM a
|
||||
## container runtime process domain.
|
||||
## </summary>
|
||||
## <param name="prefix">
|
||||
## <summary>
|
||||
## Prefix for the domain.
|
||||
## </summary>
|
||||
## </param>
|
||||
## <param name="type">
|
||||
## <summary>
|
||||
## type of process domain.
|
||||
## </summary>
|
||||
## </param>
|
||||
#
|
||||
interface(`qm_container_template',`
|
||||
# Container $2 - Policy for running systemd based containers
|
||||
type $1_container_$2_t, $1_container_domain;
|
||||
domain_type($1_container_$2_t)
|
||||
domain_user_exemption_target($1_container_$2_t)
|
||||
typeattribute $1_container_$2_t container_net_domain, container_user_domain;
|
||||
|
||||
corenet_unconfined($1_container_$2_t)
|
||||
|
||||
allow $1_container_$2_t proc_t:filesystem remount;
|
||||
|
||||
optional_policy(`
|
||||
virt_default_capabilities($1_container_$2_t)
|
||||
')
|
||||
|
||||
allow $1_container_$2_t self:netlink_audit_socket nlmsg_relay;
|
||||
container_manage_files_template($1_container_$2, $1_container)
|
||||
|
||||
read_files_pattern($1_container_$2_t, $1_container_ro_file_t, $1_container_ro_file_t)
|
||||
read_lnk_files_pattern($1_container_$2_t, $1_container_ro_file_t, $1_container_ro_file_t)
|
||||
list_dirs_pattern($1_container_$2_t, $1_container_ro_file_t, $1_container_ro_file_t)
|
||||
')
|
||||
|
||||
########################################
|
||||
## <summary>
|
||||
## Connect to IPC containers over a unix stream socket.
|
||||
## </summary>
|
||||
## <param name="domain">
|
||||
## <summary>
|
||||
## Domain allowed access.
|
||||
## </summary>
|
||||
## </param>
|
||||
#
|
||||
interface(`container_ipc_stream_connect',`
|
||||
gen_require(`
|
||||
type ipc_t, ipc_var_run_t;
|
||||
')
|
||||
|
||||
files_search_pids($1)
|
||||
stream_connect_pattern($1, ipc_var_run_t, ipc_var_run_t, ipc_t)
|
||||
rw_sock_files_pattern($1, ipc_var_run_t, ipc_var_run_t)
|
||||
')
|
||||
|
|
118
qm.spec.rpkg
118
qm.spec.rpkg
|
@ -1,118 +0,0 @@
|
|||
# For automatic rebuilds in COPR
|
||||
|
||||
# The following tag is to get correct syntax highlighting for this file in vim text editor
|
||||
# vim: syntax=spec
|
||||
|
||||
%global debug_package %{nil}
|
||||
|
||||
# Some bits borrowed from the openstack-selinux package
|
||||
%global selinuxtype targeted
|
||||
%global moduletype services
|
||||
%global modulenames qm
|
||||
|
||||
%global _installscriptdir %{_prefix}/lib/qm
|
||||
|
||||
# Usage: _format var format
|
||||
# Expand 'modulenames' into various formats as needed
|
||||
# Format must contain '$x' somewhere to do anything useful
|
||||
%global _format() export %1=""; for x in %{modulenames}; do %1+=%2; %%1+=" "; done;
|
||||
|
||||
Name: {{{ git_dir_name }}}
|
||||
Epoch: 101
|
||||
Version: {{{ git_dir_version }}}
|
||||
Release: 1%{?dist}
|
||||
License: GPLv2
|
||||
URL: https://github.com/containers/qm
|
||||
Summary: Containerized environment for running functionally safe QM (Quality Management) software.
|
||||
VCS: {{{ git_dir_vcs }}}
|
||||
Source: {{{ git_dir_pack }}}
|
||||
BuildArch: noarch
|
||||
BuildRequires: go-md2man
|
||||
BuildRequires: container-selinux
|
||||
BuildRequires: make
|
||||
BuildRequires: git-core
|
||||
BuildRequires: pkgconfig(systemd)
|
||||
BuildRequires: selinux-policy >= %_selinux_policy_version
|
||||
BuildRequires: selinux-policy-devel >= %_selinux_policy_version
|
||||
Requires: selinux-policy >= %_selinux_policy_version
|
||||
Requires(post): selinux-policy-base >= %_selinux_policy_version
|
||||
Requires(post): selinux-policy-targeted >= %_selinux_policy_version
|
||||
Requires(post): policycoreutils
|
||||
Requires(post): libselinux-utils
|
||||
Requires: podman >= 5:4.5
|
||||
Requires: hirte-agent
|
||||
|
||||
%description
|
||||
This package allow users to setup an environment which prevents applications
|
||||
and container tools from interfering with other all other processes on the
|
||||
system.
|
||||
|
||||
The QM runs its own version of systemd and Podman to isolate not only the
|
||||
applications and containers launched by systemd and Podman but systemd and
|
||||
Podman themselves.
|
||||
|
||||
Software install into the QM environment under /usr/lib/qm/rootfs is
|
||||
automatically isolated from the host. If developers need to further
|
||||
isolate there applications from other processes in the QM they should
|
||||
use container tools like Podman.
|
||||
|
||||
%prep
|
||||
{{{ git_dir_setup_macro }}}
|
||||
sed -i 's/install: man/install:/' Makefile
|
||||
|
||||
# Remove unavailable tokens
|
||||
%if 0%{?fedora} <= 37 || 0%{?rhel} <= 9
|
||||
sed -i '/user_namespace/d' qm.if
|
||||
%endif
|
||||
|
||||
%build
|
||||
%{__make}
|
||||
|
||||
%install
|
||||
# install policy modules
|
||||
%_format MODULES $x.pp.bz2
|
||||
%{__make} DESTDIR=%{buildroot} DATADIR=%{_datadir} install
|
||||
|
||||
%pre
|
||||
|
||||
%post
|
||||
# Install all modules in a single transaction
|
||||
%_format MODULES %{_datadir}/selinux/packages/$x.pp.bz2
|
||||
%selinux_modules_install -s %{selinuxtype} $MODULES
|
||||
|
||||
# Set AllowedCPUs in quadlet file
|
||||
NPROC=$(nproc)
|
||||
if [[ $NPROC == 2 ]]; then
|
||||
ALLOWED_CPUS=1
|
||||
else
|
||||
ALLOWED_CPUS=$(expr $NPROC / 2)"-"$(expr $NPROC - 1)
|
||||
fi
|
||||
sed -i "s/^AllowedCPUs=.*/AllowedCPUs=$ALLOWED_CPUS/" %{_datadir}/containers/systemd/qm.container
|
||||
|
||||
|
||||
%postun
|
||||
if [ $1 -eq 0 ]; then
|
||||
%selinux_modules_uninstall -s %{selinuxtype} %{modulenames}
|
||||
fi
|
||||
|
||||
%posttrans
|
||||
|
||||
#define license tag if not already defined
|
||||
%{!?_licensedir:%global license %doc}
|
||||
|
||||
%files
|
||||
%doc README.md
|
||||
%dir %{_datadir}/selinux
|
||||
%{_datadir}/selinux/*
|
||||
%dir %{_datadir}/qm
|
||||
%{_datadir}/qm/containers.conf
|
||||
%{_datadir}/qm/contexts
|
||||
%{_datadir}/qm/file_contexts
|
||||
%{_datadir}/qm/setup
|
||||
%ghost %dir %{_datadir}/containers
|
||||
%ghost %dir %{_datadir}/containers/systemd
|
||||
%config(noreplace) %{_datadir}/containers/systemd/qm.container
|
||||
%{_mandir}/man8/*
|
||||
|
||||
%changelog
|
||||
{{{ git_dir_changelog }}}
|
84
qm.te
84
qm.te
|
@ -1,3 +1,85 @@
|
|||
policy_module(qm, 0.1.0)
|
||||
policy_module(qm, 0.9)
|
||||
|
||||
gen_require(`
|
||||
attribute container_file_type;
|
||||
attribute container_runtime_domain;
|
||||
type init_t;
|
||||
')
|
||||
|
||||
type ipc_t;
|
||||
domain_type(ipc_t)
|
||||
role system_r types ipc_t;
|
||||
unconfined_domain_noaudit(ipc_t)
|
||||
|
||||
type ipc_exec_t;
|
||||
application_executable_file(ipc_exec_t)
|
||||
allow ipc_t { ipc_exec_t container_file_type}:file entrypoint;
|
||||
|
||||
init_system_domain(ipc_t, ipc_exec_t)
|
||||
role system_r types ipc_t;
|
||||
domtrans_pattern(container_runtime_domain, ipc_exec_t, ipc_t)
|
||||
|
||||
type ipc_var_run_t;
|
||||
files_pid_file(ipc_var_run_t)
|
||||
files_mountpoint(ipc_var_run_t)
|
||||
|
||||
files_pid_filetrans(ipc_t, ipc_var_run_t, { dir file lnk_file sock_file })
|
||||
files_pid_filetrans(init_t, ipc_var_run_t, dir, "ipc")
|
||||
|
||||
unconfined_domain(ipc_t)
|
||||
|
||||
qm_domain_template(qm)
|
||||
|
||||
|
||||
|
||||
#########################################
|
||||
#
|
||||
# bluechi-agent inside QM
|
||||
#
|
||||
|
||||
type qm_bluechi_agent_t;
|
||||
type qm_bluechi_agent_exec_t;
|
||||
init_daemon_domain(qm_bluechi_agent_t, qm_bluechi_agent_exec_t)
|
||||
|
||||
allow qm_bluechi_agent_t qm_file_t:chr_file read;
|
||||
allow qm_bluechi_agent_t qm_file_t:dir { open read search getattr };
|
||||
allow qm_bluechi_agent_t qm_file_t:file { execute getattr open read };
|
||||
allow qm_bluechi_agent_t qm_file_t:file map;
|
||||
allow qm_bluechi_agent_t qm_file_t:lnk_file read;
|
||||
allow qm_bluechi_agent_t qm_file_t:sock_file write;
|
||||
allow qm_bluechi_agent_t qm_t:unix_dgram_socket sendto;
|
||||
allow qm_bluechi_agent_t qm_t:unix_stream_socket connectto;
|
||||
allow qm_bluechi_agent_t self:unix_dgram_socket { create getopt setopt };
|
||||
allow qm_bluechi_agent_t self:tcp_socket create_stream_socket_perms;
|
||||
|
||||
allow qm_bluechi_agent_t qm_t:dbus { send_msg acquire_svc };
|
||||
allow qm_bluechi_agent_t qm_t:system status;
|
||||
allow qm_bluechi_agent_t qm_t:system { reload start stop status };
|
||||
allow qm_bluechi_agent_t qm_file_t:service { reload start stop status };
|
||||
|
||||
allow qm_t qm_bluechi_agent_t:dir search;
|
||||
allow qm_t qm_bluechi_agent_t:file { getattr ioctl open read };
|
||||
allow qm_t qm_bluechi_agent_t:lnk_file read;
|
||||
allow qm_t qm_bluechi_agent_t:dbus send_msg;
|
||||
allow qm_t qm_bluechi_agent_t:process { signull signal sigkill };
|
||||
|
||||
unconfined_server_stream_connectto(qm_bluechi_agent_t)
|
||||
|
||||
# Allow qm_bluechi_agent_t to connect to any port instead of labelled ones.
|
||||
gen_tunable(qm_bluechi_agent_port_connect_any, true)
|
||||
|
||||
optional_policy(`
|
||||
require{
|
||||
type bluechi_var_run_t;
|
||||
type bluechi_agent_port_t;
|
||||
type bluechi_t;
|
||||
}
|
||||
|
||||
tunable_policy(`qm_bluechi_agent_port_connect_any',`
|
||||
corenet_tcp_connect_all_ports(qm_bluechi_agent_t)
|
||||
',`
|
||||
allow qm_bluechi_agent_t bluechi_agent_port_t:tcp_socket name_connect;
|
||||
')
|
||||
|
||||
stream_connect_pattern(qm_bluechi_agent_t, bluechi_var_run_t, bluechi_var_run_t, bluechi_t)
|
||||
')
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
qm-kvm.spec
|
|
@ -0,0 +1 @@
|
|||
*.tar.gz
|
|
@ -0,0 +1,48 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
# rootfs_qm, Define rootfs macro for QM environment not need, do to install command from host
|
||||
# dnf install --setopt=reposdir=/etc/yum.repos.d <package>
|
||||
# using qm_sysconfdir /etc/qm/ overlay preventing the detection of quadletfile, need to install
|
||||
# in the overlay of etc under the host
|
||||
%define qm_sysconfdir %{_sysconfdir}/qm
|
||||
|
||||
Name: qm-kvm
|
||||
# Version: 0
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: Drop-in configuration for QM containers to mount bind /dev/kvm
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-kvm-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
Requires: qm >= %{version}
|
||||
|
||||
%description -n qm-kvm
|
||||
This subpackage provides a drop-in configuration for the QM environment to enable mount binding of `/dev/kvm` from the host system to containers. This configuration is essential for supporting KVM-based virtualization within QM containers.
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-kvm-%{version}
|
||||
|
||||
%build
|
||||
|
||||
%install
|
||||
# Create the directory for drop-in configurations
|
||||
install -d %{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d
|
||||
install -d %{buildroot}%{qm_sysconfdir}/containers/systemd
|
||||
|
||||
# Install the KVM drop-in configuration file
|
||||
install -m 644 %{_builddir}/qm-kvm-%{version}/etc/containers/systemd/qm.container.d/qm_dropin_mount_bind_kvm.conf \
|
||||
%{buildroot}%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_kvm.conf
|
||||
install -m 644 %{_builddir}/qm-kvm-%{version}/subsystems/kvm/etc/containers/systemd/kvm.container \
|
||||
%{buildroot}%{qm_sysconfdir}/containers/systemd/kvm.container
|
||||
|
||||
%files -n qm-kvm
|
||||
%license LICENSE
|
||||
%doc README.md SECURITY.md
|
||||
%{_sysconfdir}/containers/systemd/qm.container.d/qm_dropin_mount_bind_kvm.conf
|
||||
%{qm_sysconfdir}/containers/systemd/kvm.container
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Initial standalone spec for the QM KVM subpackage.
|
|
@ -0,0 +1,120 @@
|
|||
%global debug_package %{nil}
|
||||
|
||||
# Define path macro for QM rootfs
|
||||
%global qm_rootfs_prefix /usr/lib/qm/rootfs
|
||||
|
||||
Name: qm-oci-hooks
|
||||
Version: %{version}
|
||||
Release: 1%{?dist}
|
||||
Summary: OCI hooks for QM containers
|
||||
License: GPL-2.0-only
|
||||
URL: https://github.com/containers/qm
|
||||
Source0: %{url}/archive/qm-oci-hooks-%{version}.tar.gz
|
||||
BuildArch: noarch
|
||||
|
||||
# Runtime dependencies
|
||||
Requires: qm >= %{version}
|
||||
Requires: jq
|
||||
|
||||
%description
|
||||
This subpackage provides OCI hooks for QM containers that enable dynamic
|
||||
device access and Wayland display server integration. The hooks are installed
|
||||
both on the host system and inside the QM rootfs to support nested containers.
|
||||
|
||||
Included hooks:
|
||||
- qm-device-manager: Dynamic device mounting based on container annotations
|
||||
- wayland-client-devices: GPU hardware acceleration for Wayland clients
|
||||
|
||||
The hooks are available in two locations:
|
||||
- Host system: /usr/libexec/oci/hooks.d/ and /usr/share/containers/oci/hooks.d/
|
||||
- QM rootfs: /usr/lib/qm/rootfs/usr/libexec/oci/hooks.d/ and /usr/lib/qm/rootfs/usr/share/containers/oci/hooks.d/
|
||||
|
||||
%prep
|
||||
%autosetup -Sgit -n qm-oci-hooks-%{version}
|
||||
|
||||
%build
|
||||
# No build required for OCI hooks
|
||||
|
||||
%install
|
||||
# Create OCI hook directories
|
||||
install -d %{buildroot}%{_libexecdir}/oci/hooks.d
|
||||
install -d %{buildroot}%{_libexecdir}/oci/lib
|
||||
install -d %{buildroot}%{_datadir}/containers/oci/hooks.d
|
||||
|
||||
# Note: QM rootfs directories (/usr/lib/qm/rootfs/*) are handled by ghost directories in main qm package
|
||||
# We only need to create the specific directories we're installing into
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib
|
||||
install -d %{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d
|
||||
|
||||
# Install QM Device Manager hook
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager \
|
||||
%{buildroot}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager.json \
|
||||
%{buildroot}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/oci-qm-device-manager.json \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
|
||||
# Install Wayland Client Devices hook
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices \
|
||||
%{buildroot}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json \
|
||||
%{buildroot}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
install -m 755 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/oci-qm-wayland-client-devices.json \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
# Install common libraries
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/common.sh \
|
||||
%{buildroot}%{_libexecdir}/oci/lib/common.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/device-support.sh \
|
||||
%{buildroot}%{_libexecdir}/oci/lib/device-support.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/common.sh \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/common.sh
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/lib/device-support.sh \
|
||||
%{buildroot}%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# Create documentation directory and install component-specific README files with unique names
|
||||
install -d %{buildroot}%{_docdir}/qm-oci-hooks
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/qm-device-manager/README.md \
|
||||
%{buildroot}%{_docdir}/qm-oci-hooks/README-qm-device-manager.md
|
||||
install -m 644 %{_builddir}/qm-oci-hooks-%{version}/oci-hooks/wayland-client-devices/README.md \
|
||||
%{buildroot}%{_docdir}/qm-oci-hooks/README-wayland-client-devices.md
|
||||
|
||||
%files
|
||||
%license LICENSE
|
||||
%doc CODE-OF-CONDUCT.md README.md SECURITY.md
|
||||
%{_docdir}/qm-oci-hooks/README-qm-device-manager.md
|
||||
%{_docdir}/qm-oci-hooks/README-wayland-client-devices.md
|
||||
|
||||
# OCI hook executables and libraries (host system)
|
||||
%dir %{_libexecdir}/oci
|
||||
%dir %{_libexecdir}/oci/hooks.d
|
||||
%dir %{_libexecdir}/oci/lib
|
||||
%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
%{_libexecdir}/oci/lib/common.sh
|
||||
%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# OCI hook configurations (host system)
|
||||
%dir %{_datadir}/containers/oci
|
||||
%dir %{_datadir}/containers/oci/hooks.d
|
||||
%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
# OCI hook executables and libraries (QM rootfs for nested containers)
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-device-manager
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/hooks.d/oci-qm-wayland-client-devices
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/common.sh
|
||||
%{qm_rootfs_prefix}%{_libexecdir}/oci/lib/device-support.sh
|
||||
|
||||
# OCI hook configurations (QM rootfs for nested containers)
|
||||
%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-device-manager.json
|
||||
%{qm_rootfs_prefix}%{_datadir}/containers/oci/hooks.d/oci-qm-wayland-client-devices.json
|
||||
|
||||
%changelog
|
||||
* Fri Jul 21 2023 RH Container Bot <rhcontainerbot@fedoraproject.org>
|
||||
- Initial release of consolidated QM OCI hooks package
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue