Compare commits
621 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c75c55fa4 | ||
|
|
b9741e87bd | ||
|
|
c555478b54 | ||
|
|
3718372288 | ||
|
|
390b41bc26 | ||
|
|
530c5a64fb | ||
|
|
d285aaedc9 | ||
|
|
453fe18d7f | ||
|
|
5fae5cd308 | ||
|
|
7d7e225823 | ||
|
|
19f404799d | ||
|
|
3e4652dca3 | ||
|
|
45b08de874 | ||
|
|
310e26dda9 | ||
|
|
f4eb54b835 | ||
|
|
3968c12947 | ||
|
|
21c97d255f | ||
|
|
eb1053607a | ||
|
|
de7198e9dc | ||
|
|
0f30f47249 | ||
|
|
6b2ad8ebc8 | ||
|
|
1f302144ef | ||
|
|
d04c7a6ae4 | ||
|
|
9ca2cda8c7 | ||
|
|
1fd06ecbf9 | ||
|
|
97baad4cb1 | ||
|
|
fbd93ecf0d | ||
|
|
e941ccea92 | ||
|
|
d692483bc3 | ||
|
|
95cfbd0fff | ||
|
|
b3d1ed9e65 | ||
|
|
fe2b8c9fee | ||
|
|
2d7deef4e2 | ||
|
|
b4a70b02e3 | ||
|
|
c5c2364ed4 | ||
|
|
efcc4291a3 | ||
|
|
6ea6ee264d | ||
|
|
2865ba7632 | ||
|
|
2bed668d31 | ||
|
|
9dc24860f3 | ||
|
|
f01377b3bc | ||
|
|
7443dfac4c | ||
|
|
e6408e187c | ||
|
|
a02d282d3e | ||
|
|
f778f53744 | ||
|
|
95ea93564e | ||
|
|
d51029e86c | ||
|
|
1016699c94 | ||
|
|
63f63955e7 | ||
|
|
37be9fda9f | ||
|
|
0756133a7e | ||
|
|
83c5ab318b | ||
|
|
0c28957016 | ||
|
|
959084040d | ||
|
|
8a428c6936 | ||
|
|
48da23226c | ||
|
|
5f0c6e5102 | ||
|
|
29f1c6f50e | ||
|
|
4d187419ac | ||
|
|
58998f9cab | ||
|
|
cdc5ca8854 | ||
|
|
44e1e41266 | ||
|
|
33fda8605a | ||
|
|
5f9ed69299 | ||
|
|
7f1baea3b0 | ||
|
|
f75026e4b2 | ||
|
|
ce7a1a9c67 | ||
|
|
a62056fb19 | ||
|
|
f3434a8155 | ||
|
|
4e023ef517 | ||
|
|
97b80cb930 | ||
|
|
525b4e6a53 | ||
|
|
054eaf65b8 | ||
|
|
48fa796ab1 | ||
|
|
1873e022cc | ||
|
|
35a8062b58 | ||
|
|
636248ad67 | ||
|
|
4511c54fad | ||
|
|
7f3970541b | ||
|
|
4040f4d151 | ||
|
|
887d374c53 | ||
|
|
be4b87155d | ||
|
|
b987a7da4c | ||
|
|
7153fe5ad2 | ||
|
|
ccd8ba44d9 | ||
|
|
e7ef0f7a6c | ||
|
|
400b58c0e9 | ||
|
|
5257496214 | ||
|
|
e1bfe4a3ce | ||
|
|
f31cce8ec2 | ||
|
|
169ebfa72c | ||
|
|
7cace52ab5 | ||
|
|
95b93c60d9 | ||
|
|
5af1dcb8b1 | ||
|
|
6a61774fb7 | ||
|
|
ccbaca24f1 | ||
|
|
07b6048dc5 | ||
|
|
60dd34d066 | ||
|
|
28451d1e14 | ||
|
|
db95b6381f | ||
|
|
6b14c9bea4 | ||
|
|
742adc00fe | ||
|
|
52897cc16c | ||
|
|
c950568f1b | ||
|
|
845d7ff188 | ||
|
|
3bd8658da6 | ||
|
|
336a38081a | ||
|
|
01c2131436 | ||
|
|
c274231544 | ||
|
|
4a2864701c | ||
|
|
76ede10e0a | ||
|
|
274e01bb75 | ||
|
|
d75f763c99 | ||
|
|
5bc985663c | ||
|
|
df9e2e853f | ||
|
|
b4828a6f0a | ||
|
|
e99dd749a0 | ||
|
|
10ce7178c0 | ||
|
|
5c6a66eaf5 | ||
|
|
36d30bc985 | ||
|
|
a5152b82e9 | ||
|
|
e9af8a2595 | ||
|
|
84b5b60d49 | ||
|
|
8f60f42be3 | ||
|
|
583344138a | ||
|
|
016d021d5a | ||
|
|
115dc4bfa4 | ||
|
|
5b83febb23 | ||
|
|
c9d5c50402 | ||
|
|
fc839d2983 | ||
|
|
3bce96bbd5 | ||
|
|
6279be073b | ||
|
|
ea37132ce4 | ||
|
|
70eecd5289 | ||
|
|
380d03257f | ||
|
|
006de6da14 | ||
|
|
10aa80e8f5 | ||
|
|
013439af6d | ||
|
|
3408961155 | ||
|
|
f3b4a8d055 | ||
|
|
104af7e86f | ||
|
|
be39fbeff6 | ||
|
|
4109045fa4 | ||
|
|
90fd8023dd | ||
|
|
f67ad9c061 | ||
|
|
525e2bafee | ||
|
|
b65a9abf8e | ||
|
|
fec94aa53a | ||
|
|
3d4b345728 | ||
|
|
579975f08d | ||
|
|
3707b39fef | ||
|
|
f07387225b | ||
|
|
2648fb1bb1 | ||
|
|
d34715b4ba | ||
|
|
63af50bf98 | ||
|
|
456550c1d4 | ||
|
|
8174b88ec3 | ||
|
|
3233973748 | ||
|
|
bdfb1cf33e | ||
|
|
1c5fcd59e7 | ||
|
|
5cc960527e | ||
|
|
762c53fb8d | ||
|
|
ff20e67d07 | ||
|
|
c0cea013d1 | ||
|
|
5526bbba64 | ||
|
|
f0aa96ea8c | ||
|
|
e73007c398 | ||
|
|
fdc459ec5b | ||
|
|
bdb523ece1 | ||
|
|
164a9479ad | ||
|
|
e18adc781f | ||
|
|
33d89c2739 | ||
|
|
7cc9ab9083 | ||
|
|
4b4b7dc169 | ||
|
|
71ad5c5f05 | ||
|
|
39368bb5cb | ||
|
|
7a587ee8d1 | ||
|
|
77346527f3 | ||
|
|
1eba5833d5 | ||
|
|
83a747794e | ||
|
|
3e16d1da46 | ||
|
|
ae1860e859 | ||
|
|
2ebc8fdf2a | ||
|
|
be4023be66 | ||
|
|
7f4ad76298 | ||
|
|
0cbfaf98f3 | ||
|
|
631124e658 | ||
|
|
1685ee1ecb | ||
|
|
9b4d11f220 | ||
|
|
46a71296a9 | ||
|
|
1285588b62 | ||
|
|
d96392f65e | ||
|
|
d1c5a736ae | ||
|
|
6b1e038c5c | ||
|
|
eaab1aae28 | ||
|
|
31030343a2 | ||
|
|
325ca03a13 | ||
|
|
dea8e63df2 | ||
|
|
58421fd31a | ||
|
|
b961c96862 | ||
|
|
2d23c1b0f3 | ||
|
|
06952c224b | ||
|
|
2ea492c965 | ||
|
|
dbf84f6879 | ||
|
|
0fa3d6c462 | ||
|
|
d57f7aa03f | ||
|
|
d64f9f5401 | ||
|
|
a3029afc41 | ||
|
|
6a7d904fae | ||
|
|
d4043d3f86 | ||
|
|
b4902a4f58 | ||
|
|
ffe402f201 | ||
|
|
09cc7da282 | ||
|
|
2d2dad41f4 | ||
|
|
5f7c0a86dd | ||
|
|
fc1c631c98 | ||
|
|
89bdafacb8 | ||
|
|
73b6b3f129 | ||
|
|
b2a495f593 | ||
|
|
65ee904377 | ||
|
|
13f59230b5 | ||
|
|
36d2a0de1e | ||
|
|
a4db9fc8e5 | ||
|
|
9dae5ef83b | ||
|
|
e8842a740c | ||
|
|
0d3807ad09 | ||
|
|
5c27a249b7 | ||
|
|
7e41860b28 | ||
|
|
43ff92bbe7 | ||
|
|
28adc7e563 | ||
|
|
9788411995 | ||
|
|
0c9e8cc50e | ||
|
|
34d572c523 | ||
|
|
011b496b3f | ||
|
|
12b906eac6 | ||
|
|
20937d05c3 | ||
|
|
4943d37ccf | ||
|
|
3a8fd215de | ||
|
|
87572e8922 | ||
|
|
f1eedc7a01 | ||
|
|
b79e48dd77 | ||
|
|
18872194af | ||
|
|
bafd7ba282 | ||
|
|
b186481181 | ||
|
|
09ca6d11ad | ||
|
|
e68e4e786d | ||
|
|
ee638254c3 | ||
|
|
1e678905c4 | ||
|
|
10804c4b25 | ||
|
|
4bf9b4d41b | ||
|
|
1161872324 | ||
|
|
98cb570896 | ||
|
|
ed4ee3b58e | ||
|
|
066048f4de | ||
|
|
4b6b91c08b | ||
|
|
2980523a5b | ||
|
|
f2f9c043bf | ||
|
|
5d59cfd2c9 | ||
|
|
f94474e24f | ||
|
|
a63fc6d9ba | ||
|
|
076adeef80 | ||
|
|
a0e756317c | ||
|
|
252cb5f2f3 | ||
|
|
64288b4914 | ||
|
|
9ca6c6a315 | ||
|
|
3651ab5c0c | ||
|
|
b3f15e1ddc | ||
|
|
da2a5f72bd | ||
|
|
591e6b68e0 | ||
|
|
0119abdcdd | ||
|
|
e57ca15330 | ||
|
|
f53376cea1 | ||
|
|
4f1c463bdd | ||
|
|
6643a3d937 | ||
|
|
da8cb40242 | ||
|
|
4c6d304e60 | ||
|
|
99d3ef42e9 | ||
|
|
e2289dc2a0 | ||
|
|
9b4f50cde9 | ||
|
|
fe64bd9dbb | ||
|
|
0991264c8c | ||
|
|
3b608ad544 | ||
|
|
3f1a379908 | ||
|
|
61a67dae29 | ||
|
|
609aefd808 | ||
|
|
191a2495a5 | ||
|
|
a235b760dc | ||
|
|
e4eb3c23a2 | ||
|
|
12582e963d | ||
|
|
d5074871c7 | ||
|
|
e0d024ac95 | ||
|
|
7a756cacb9 | ||
|
|
3c1da423fa | ||
|
|
38dfaa1caa | ||
|
|
a050cff50f | ||
|
|
93c1b37aab | ||
|
|
01d4226c4a | ||
|
|
fc6032d3b7 | ||
|
|
43839d1090 | ||
|
|
b3632584c3 | ||
|
|
e9257580cd | ||
|
|
e3cc6309ea | ||
|
|
17fd625f7f | ||
|
|
d1ecfd8657 | ||
|
|
4aa3cfad40 | ||
|
|
3bcb697662 | ||
|
|
88318b73e4 | ||
|
|
2f7e202f40 | ||
|
|
310239e707 | ||
|
|
4de75373dd | ||
|
|
c0d329e6d8 | ||
|
|
8a0840d35b | ||
|
|
f9bb9ef33e | ||
|
|
acb2a5d2b0 | ||
|
|
63ef11c708 | ||
|
|
d70bbfb5d0 | ||
|
|
97d60ac98d | ||
|
|
8f1f5d33fd | ||
|
|
d65c85c19f | ||
|
|
22d893fc1e | ||
|
|
806d2f6a4a | ||
|
|
fc3baa28d6 | ||
|
|
eba45e6207 | ||
|
|
272fd3edc3 | ||
|
|
5ad8b33aa7 | ||
|
|
cacd14fcf8 | ||
|
|
859e4749ae | ||
|
|
a5842a41b2 | ||
|
|
fb275d9537 | ||
|
|
88f7b7e786 | ||
|
|
30402effa9 | ||
|
|
7d96623f06 | ||
|
|
398706246e | ||
|
|
6628fc02f2 | ||
|
|
e2fa7f59a1 | ||
|
|
d5b7dc0acc | ||
|
|
e4d874cc69 | ||
|
|
80a0abeead | ||
|
|
0df2d46ca7 | ||
|
|
07f542b4d7 | ||
|
|
7db3e8556a | ||
|
|
dc03e67b81 | ||
|
|
e587324b81 | ||
|
|
65a66492f4 | ||
|
|
17602d7065 | ||
|
|
ae56261961 | ||
|
|
c4f57608d0 | ||
|
|
753d1104ef | ||
|
|
880652f5d4 | ||
|
|
54c81d6bb2 | ||
|
|
2bf43eae24 | ||
|
|
58961d23bb | ||
|
|
6044ade373 | ||
|
|
da1c96c6fd | ||
|
|
5bbb474db6 | ||
|
|
a0c909914d | ||
|
|
170e56b34a | ||
|
|
de43569fa2 | ||
|
|
aa6b701b77 | ||
|
|
d69eb27557 | ||
|
|
0ca57d6a09 | ||
|
|
4c104d55cb | ||
|
|
8a8215fabe | ||
|
|
4badeafb98 | ||
|
|
7cb79bec49 | ||
|
|
8da0da02d9 | ||
|
|
efef260764 | ||
|
|
a56991d081 | ||
|
|
f0196540ab | ||
|
|
426b15313e | ||
|
|
11fc55d679 | ||
|
|
de1691665f | ||
|
|
b1f93b40ae | ||
|
|
5e58251026 | ||
|
|
4f4091a9bd | ||
|
|
e9fb41fdc8 | ||
|
|
6b803656b2 | ||
|
|
829741e2ac | ||
|
|
94c40909cc | ||
|
|
95dab16e6e | ||
|
|
c049413b47 | ||
|
|
2d45f95501 | ||
|
|
3cfc76b635 | ||
|
|
d88874845c | ||
|
|
5e38c1c8fe | ||
|
|
ae7ebeedd1 | ||
|
|
652b657809 | ||
|
|
62a6e0da1d | ||
|
|
0d0d48d9f6 | ||
|
|
ab5957f1e9 | ||
|
|
463ba23003 | ||
|
|
ccad6e7e1a | ||
|
|
aa165b5e17 | ||
|
|
f06e87377c | ||
|
|
4c3bf9fc7a | ||
|
|
253ed78cc6 | ||
|
|
4860d833c7 | ||
|
|
450d5c1a59 | ||
|
|
88fcda2c99 | ||
|
|
00db953c9f | ||
|
|
a0df4829a8 | ||
|
|
b0e1f12c22 | ||
|
|
ee56155ec4 | ||
|
|
16d7c6a933 | ||
|
|
f7a06c1da9 | ||
|
|
4c8086977a | ||
|
|
b1f088e5fa | ||
|
|
1247c789aa | ||
|
|
749038c76d | ||
|
|
0a052494c4 | ||
|
|
90fa83a5cf | ||
|
|
4eaff892c1 | ||
|
|
f368f75209 | ||
|
|
04048b13ed | ||
|
|
5acc33c751 | ||
|
|
b449be89a7 | ||
|
|
dac019290d | ||
|
|
bdc424e39d | ||
|
|
10193a2796 | ||
|
|
2c9a12e941 | ||
|
|
8ba6c40f0c | ||
|
|
bbfeb49cdf | ||
|
|
f61e1cb36d | ||
|
|
4a3e2c3611 | ||
|
|
81faec508c | ||
|
|
9966ca2e85 | ||
|
|
35c26f9ee5 | ||
|
|
b5e29771ab | ||
|
|
f5f09d3640 | ||
|
|
5a531b7948 | ||
|
|
f716a3a73b | ||
|
|
ce8c8c8eea | ||
|
|
fc48fda7e5 | ||
|
|
78936c5ce8 | ||
|
|
5d0efce278 | ||
|
|
0c17a0b4f2 | ||
|
|
3f396a7c52 | ||
|
|
8697f8f91f | ||
|
|
06c67685f1 | ||
|
|
dc2e7de9e5 | ||
|
|
db1dbe7a27 | ||
|
|
d6bbb94be5 | ||
|
|
e3b4c0aee3 | ||
|
|
a1fbe152bb | ||
|
|
9d28ff9b23 | ||
|
|
43f0ddd25d | ||
|
|
7a28b00d75 | ||
|
|
32e29862f2 | ||
|
|
6c5c38f5a7 | ||
|
|
2da7854b24 | ||
|
|
6d0c5ab2d5 | ||
|
|
9398deeabc | ||
|
|
bf63d2e844 | ||
|
|
b808592fb3 | ||
|
|
e2296a631b | ||
|
|
e20555d4bc | ||
|
|
b89e2dcd3c | ||
|
|
165d11b2ca | ||
|
|
d4046c0acf | ||
|
|
88498695ac | ||
|
|
354a1c23b0 | ||
|
|
34550246f4 | ||
|
|
db1cc846dc | ||
|
|
74484bcbdf | ||
|
|
d5ecf8ce16 | ||
|
|
b1ffb1d4a4 | ||
|
|
451e1122a7 | ||
|
|
10dcf32f3c | ||
|
|
7f1477b26d | ||
|
|
33b68c09d3 | ||
|
|
7ec48ca845 | ||
|
|
5c92cef983 | ||
|
|
75eba466c6 | ||
|
|
ad30737119 | ||
|
|
8e0bde3071 | ||
|
|
7d641427d2 | ||
|
|
3b62beed26 | ||
|
|
2d3cf68261 | ||
|
|
7d6080d13f | ||
|
|
e3eefeb3fe | ||
|
|
f10dddadd6 | ||
|
|
d166112917 | ||
|
|
8ed5c1bedf | ||
|
|
4489076fac | ||
|
|
bdc33cd421 | ||
|
|
889dae2955 | ||
|
|
9ff21b68e4 | ||
|
|
a69a7009f8 | ||
|
|
d413fac4cb | ||
|
|
246ecd8607 | ||
|
|
22105af720 | ||
|
|
880c4d2f48 | ||
|
|
443f489152 | ||
|
|
39fdfdfd8c | ||
|
|
96dccca475 | ||
|
|
948a3c6d08 | ||
|
|
dc13d5d26b | ||
|
|
aae714db6b | ||
|
|
a7c9673bcf | ||
|
|
3d06775ddc | ||
|
|
48beea3884 | ||
|
|
958d3f6094 | ||
|
|
08f24fb272 | ||
|
|
07d57e1a64 | ||
|
|
cd7711bdfe | ||
|
|
433ffa05a5 | ||
|
|
046b21b907 | ||
|
|
c32183eb70 | ||
|
|
73b11045f2 | ||
|
|
57ce3fa587 | ||
|
|
a26620da38 | ||
|
|
86b8099eb9 | ||
|
|
c8e9a100a6 | ||
|
|
a287f028d1 | ||
|
|
cf50fb3568 | ||
|
|
4c8193876f | ||
|
|
158bc1eb2a | ||
|
|
3f42e5f702 | ||
|
|
75633817a7 | ||
|
|
83b00fce3e | ||
|
|
38befb53ad | ||
|
|
d0b5c4de68 | ||
|
|
1b68845b00 | ||
|
|
a7bc72540d | ||
|
|
27ac7481f9 | ||
|
|
9bc36be513 | ||
|
|
e62e35bc88 | ||
|
|
bd80ced9b2 | ||
|
|
bb2f2e5e54 | ||
|
|
b1eb6711b7 | ||
|
|
da0ffa5e56 | ||
|
|
68ef312233 | ||
|
|
9fefadca24 | ||
|
|
e14b14b88c | ||
|
|
d5bfb7257e | ||
|
|
8282f3b59c | ||
|
|
dbf0c84f0b | ||
|
|
a5977b993a | ||
|
|
27df3ae876 | ||
|
|
a49d07cf01 | ||
|
|
28f343ac50 | ||
|
|
4297a39d03 | ||
|
|
bd996e441c | ||
|
|
086a89fad6 | ||
|
|
70ac38e66c | ||
|
|
d990d2ad86 | ||
|
|
56db31ca43 | ||
|
|
b902e2d30b | ||
|
|
d2bab32b0e | ||
|
|
b2d726051b | ||
|
|
8e25667f87 | ||
|
|
9b5c4c50e7 | ||
|
|
d2ce70a673 | ||
|
|
9db0fc4ee4 | ||
|
|
9ed830bb81 | ||
|
|
4e42d9ed03 | ||
|
|
4c93bc3599 | ||
|
|
7c817802a8 | ||
|
|
de90b592fb | ||
|
|
b9d0cc2e28 | ||
|
|
0ec00fe57f | ||
|
|
80931e1cb4 | ||
|
|
cc02e96a13 | ||
|
|
51ec91dd16 | ||
|
|
916a92c3d8 | ||
|
|
5431bfdc29 | ||
|
|
43b5b4f5a4 | ||
|
|
f342e06ef0 | ||
|
|
81bb87f4cd | ||
|
|
c4b97fadcc | ||
|
|
05f6ba7297 | ||
|
|
c62b8a5d4f | ||
|
|
83dab30ecf | ||
|
|
24b08a332d | ||
|
|
70ccb3022a | ||
|
|
8019b90b8a | ||
|
|
5f12ff6178 | ||
|
|
6e20e48489 | ||
|
|
f29a72235c | ||
|
|
e25d499eeb | ||
|
|
9cae339546 | ||
|
|
a049af6262 | ||
|
|
a402f50f9b | ||
|
|
9f89ea9be6 | ||
|
|
e538aacf9d | ||
|
|
968c609697 | ||
|
|
c11cfa0a62 | ||
|
|
074f4677d5 | ||
|
|
9ea5c03371 | ||
|
|
22c0ff3cf5 | ||
|
|
3ced981d28 | ||
|
|
299080f590 | ||
|
|
a407771eaf | ||
|
|
d26a6de759 | ||
|
|
9baad56197 | ||
|
|
a589e2ecf3 | ||
|
|
d7029871b1 | ||
|
|
b80a505be5 | ||
|
|
412a25462e | ||
|
|
9a8408a092 | ||
|
|
86a9181e9b | ||
|
|
9969286224 | ||
|
|
ef49aa7e08 | ||
|
|
acdb497b80 | ||
|
|
4d8faeb826 | ||
|
|
6e0dfdb16f | ||
|
|
754480a9b6 | ||
|
|
15681ddca9 | ||
|
|
3c8d424a43 | ||
|
|
7d7eb3d1cd | ||
|
|
8500339ba6 | ||
|
|
06ee05026b | ||
|
|
ddefb4e987 | ||
|
|
62d1fc7ed3 | ||
|
|
f3b99b3940 | ||
|
|
97c11c18d0 | ||
|
|
93a909551f | ||
|
|
ea52eb78d9 | ||
|
|
fdd698dade | ||
|
|
173ccf6861 | ||
|
|
a5c3db6303 |
5
.github/FUNDING.yml
vendored
Normal file
5
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# You can add one username per supported platform and one custom link.
|
||||
custom: "https://paypal.me/purpleidea"
|
||||
github: purpleidea
|
||||
liberapay: purpleidea
|
||||
patreon: purpleidea
|
||||
2
.github/settings.yml
vendored
2
.github/settings.yml
vendored
@@ -68,6 +68,8 @@ labels:
|
||||
color: e11d21
|
||||
- name: question
|
||||
color: cc317c
|
||||
- name: needinfo
|
||||
color: fbca04
|
||||
- name: wontfix
|
||||
color: ffffff
|
||||
# - name: first-timers-only
|
||||
|
||||
70
.github/workflows/test.yaml
vendored
Normal file
70
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
# Docs: https://help.github.com/en/articles/workflow-syntax-for-github-actions
|
||||
|
||||
# If the name is omitted, it uses the filename instead.
|
||||
#name: Test
|
||||
on:
|
||||
# Run on all pull requests.
|
||||
pull_request:
|
||||
#branches:
|
||||
#- master
|
||||
# Run on all pushes.
|
||||
push:
|
||||
# Run daily at 4am.
|
||||
schedule:
|
||||
- cron: 0 4 * * *
|
||||
|
||||
jobs:
|
||||
maketest:
|
||||
name: Test (${{ matrix.test_block }}) on ${{ matrix.os }} with golang ${{ matrix.golang_version }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
GOPATH: /home/runner/work/mgmt/mgmt/go
|
||||
strategy:
|
||||
matrix:
|
||||
# TODO: Add tip when it's supported: https://github.com/actions/setup-go/issues/21
|
||||
os:
|
||||
- ubuntu-latest
|
||||
# macos tests are currently failing in CI
|
||||
#- macos-latest
|
||||
golang_version:
|
||||
# TODO: add 1.15.x and tip
|
||||
# minimum required and latest published go_version
|
||||
#- 1.13
|
||||
- 1.15
|
||||
test_block:
|
||||
- basic
|
||||
- shell
|
||||
- race
|
||||
#fail-fast: false
|
||||
|
||||
steps:
|
||||
# Do not shallow fetch, will fail when building bindata/
|
||||
# The path can't be absolute, so we need to move it to the
|
||||
# expected location later.
|
||||
- name: Clone mgmt
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
path: ./go/src/github.com/purpleidea/mgmt
|
||||
|
||||
- name: Install Go ${{ matrix.golang_version }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.golang_version }}
|
||||
|
||||
# Install & configure ruby, fixes gem permissions error
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: head
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./go/src/github.com/purpleidea/mgmt
|
||||
run: |
|
||||
make deps
|
||||
|
||||
- name: Run test
|
||||
working-directory: ./go/src/github.com/purpleidea/mgmt
|
||||
run: |
|
||||
TEST_BLOCK="${{ matrix.test_block }}" make test
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
.envrc
|
||||
old/
|
||||
tmp/
|
||||
*WIP
|
||||
*_stringer.go
|
||||
bindata/*.go
|
||||
mgmt
|
||||
@@ -13,4 +14,8 @@ mgmt.static
|
||||
build/mgmt-*
|
||||
mgmt.iml
|
||||
rpmbuild/
|
||||
*.deb
|
||||
releases/
|
||||
# vim swap files
|
||||
.*.sw[op]
|
||||
# prevent `echo foo 2>1` typo errors by making this file read-only
|
||||
1
|
||||
|
||||
17
.gitmodules
vendored
17
.gitmodules
vendored
@@ -1,5 +1,5 @@
|
||||
[submodule "vendor/github.com/coreos/etcd"]
|
||||
path = vendor/github.com/coreos/etcd
|
||||
path = vendor/go.etcd.io/etcd
|
||||
url = https://github.com/coreos/etcd/
|
||||
[submodule "vendor/google.golang.org/grpc"]
|
||||
path = vendor/google.golang.org/grpc
|
||||
@@ -22,3 +22,18 @@
|
||||
[submodule "vendor/github.com/ugorji/go"]
|
||||
path = vendor/github.com/ugorji/go
|
||||
url = https://github.com/ugorji/go
|
||||
[submodule "vendor/github.com/purpleidea/docker"]
|
||||
path = vendor/github.com/docker/docker
|
||||
url = https://github.com/purpleidea/docker
|
||||
[submodule "vendor/github.com/purpleidea/distribution"]
|
||||
path = vendor/github.com/docker/distribution
|
||||
url = https://github.com/purpleidea/distribution
|
||||
[submodule "vendor/github.com/hashicorp/go-multierror"]
|
||||
path = vendor/github.com/hashicorp/go-multierror
|
||||
url = https://github.com/hashicorp/go-multierror
|
||||
[submodule "vendor/github.com/containerd/containerd"]
|
||||
path = vendor/github.com/containerd/containerd
|
||||
url = https://github.com/purpleidea/containerd
|
||||
[submodule "vendor/github.com/hashicorp/consul"]
|
||||
path = vendor/github.com/hashicorp/consul
|
||||
url = https://github.com/hashicorp/consul/
|
||||
|
||||
40
.travis.yml
40
.travis.yml
@@ -1,14 +1,18 @@
|
||||
language: go
|
||||
os:
|
||||
- linux
|
||||
go:
|
||||
- 1.9.x
|
||||
- 1.10.x
|
||||
- tip
|
||||
go_import_path: github.com/purpleidea/mgmt
|
||||
sudo: true
|
||||
dist: trusty
|
||||
dist: xenial
|
||||
# travis requires that you update manually, and provides this key to trigger it
|
||||
apt:
|
||||
update: true
|
||||
before_install:
|
||||
# print some debug information to help catch the constant travis regressions
|
||||
- if [ -e /etc/apt/sources.list.d/ ]; then sudo ls -l /etc/apt/sources.list.d/; fi
|
||||
# workaround broken travis NO_PUBKEY errors
|
||||
- if [ -e /etc/apt/sources.list.d/rabbitmq_rabbitmq-server.list ]; then sudo rm -f /etc/apt/sources.list.d/rabbitmq_rabbitmq-server.list; fi
|
||||
- if [ -e /etc/apt/sources.list.d/github_git-lfs.list ]; then sudo rm -f /etc/apt/sources.list.d/github_git-lfs.list; fi
|
||||
# as per a number of comments online, this might mitigate some flaky fails...
|
||||
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6; fi
|
||||
# apt update tends to be flaky in travis, retry up to 3 times on failure
|
||||
@@ -17,24 +21,34 @@ before_install:
|
||||
- git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
|
||||
- git fetch --unshallow
|
||||
install: 'make deps'
|
||||
script: 'make test'
|
||||
matrix:
|
||||
fast_finish: false
|
||||
allow_failures:
|
||||
- go: 1.10.x
|
||||
- go: tip
|
||||
- os: osx
|
||||
- go: 1.14.x
|
||||
- go: tip
|
||||
- os: osx
|
||||
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
||||
include:
|
||||
- os: osx
|
||||
go: 1.9.x
|
||||
- name: "basic tests"
|
||||
go: 1.13.x
|
||||
env: TEST_BLOCK=basic
|
||||
- name: "shell tests"
|
||||
go: 1.13.x
|
||||
env: TEST_BLOCK=shell
|
||||
- name: "race tests"
|
||||
go: 1.13.x
|
||||
env: TEST_BLOCK=race
|
||||
- go: 1.14.x
|
||||
- go: tip
|
||||
- os: osx
|
||||
script: 'TEST_BLOCK="$TEST_BLOCK" make test'
|
||||
|
||||
# the "secure" channel value is the result of running: ./misc/travis-encrypt.sh
|
||||
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
||||
#channels:
|
||||
# - secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
||||
template:
|
||||
- "%{repository} (%{commit}: %{author}): %{message}"
|
||||
- "More info : %{build_url}"
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -6,6 +6,7 @@ This list is sorted alphabetically by first name.
|
||||
|
||||
Felix Frank
|
||||
James Shubin
|
||||
Joe Groocock
|
||||
Johan Bloemberg
|
||||
Jonathan Gold
|
||||
Julien Pivotto
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Mgmt
|
||||
Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
||||
243
Makefile
243
Makefile
@@ -1,5 +1,5 @@
|
||||
# Mgmt
|
||||
# Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
# Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
# Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,15 +16,21 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL = /usr/bin/env bash
|
||||
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
|
||||
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs
|
||||
.PHONY: rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms upload-releases copr tag
|
||||
.PHONY: mkosi mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux
|
||||
.PHONY: release releases_path release_fedora-30 release_fedora-29 release_centos-7 release_debian-10 release_ubuntu-bionic release_archlinux
|
||||
.PHONY: funcgen
|
||||
.SILENT: clean bindata
|
||||
|
||||
# a large amount of output from this `find`, can cause `make` to be much slower!
|
||||
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
|
||||
MCL_FILES := $(shell find lang/funcs/ -name '*.mcl' -not -path 'old/*' -not -path 'tmp/*')
|
||||
|
||||
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
|
||||
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
|
||||
PKGNAME := $(shell go list .)
|
||||
ifeq ($(VERSION),$(SVERSION))
|
||||
RELEASE = 1
|
||||
else
|
||||
@@ -47,12 +53,36 @@ GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
|
||||
TOKEN_FEDORA-30 = fedora-30
|
||||
TOKEN_FEDORA-29 = fedora-29
|
||||
TOKEN_CENTOS-7 = centos-7
|
||||
TOKEN_DEBIAN-10 = debian-10
|
||||
TOKEN_UBUNTU-BIONIC = ubuntu-bionic
|
||||
TOKEN_ARCHLINUX = archlinux
|
||||
|
||||
FILE_FEDORA-30 = mgmt-$(TOKEN_FEDORA-30)-$(VERSION)-1.x86_64.rpm
|
||||
FILE_FEDORA-29 = mgmt-$(TOKEN_FEDORA-29)-$(VERSION)-1.x86_64.rpm
|
||||
FILE_CENTOS-7 = mgmt-$(TOKEN_CENTOS-7)-$(VERSION)-1.x86_64.rpm
|
||||
FILE_DEBIAN-10 = mgmt_$(TOKEN_DEBIAN-10)_$(VERSION)_amd64.deb
|
||||
FILE_UBUNTU-BIONIC = mgmt_$(TOKEN_UBUNTU-BIONIC)_$(VERSION)_amd64.deb
|
||||
FILE_ARCHLINUX = mgmt-$(TOKEN_ARCHLINUX)-$(VERSION)-1-x86_64.pkg.tar.xz
|
||||
|
||||
PKG_FEDORA-30 = releases/$(VERSION)/$(TOKEN_FEDORA-30)/$(FILE_FEDORA-30)
|
||||
PKG_FEDORA-29 = releases/$(VERSION)/$(TOKEN_FEDORA-29)/$(FILE_FEDORA-29)
|
||||
PKG_CENTOS-7 = releases/$(VERSION)/$(TOKEN_CENTOS-7)/$(FILE_CENTOS-7)
|
||||
PKG_DEBIAN-10 = releases/$(VERSION)/$(TOKEN_DEBIAN-10)/$(FILE_DEBIAN-10)
|
||||
PKG_UBUNTU-BIONIC = releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/$(FILE_UBUNTU-BIONIC)
|
||||
PKG_ARCHLINUX = releases/$(VERSION)/$(TOKEN_ARCHLINUX)/$(FILE_ARCHLINUX)
|
||||
|
||||
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
|
||||
SHA256SUMS_ASC = $(SHA256SUMS).asc
|
||||
|
||||
default: build
|
||||
|
||||
#
|
||||
# art
|
||||
#
|
||||
art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
|
||||
art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png ## generate artwork
|
||||
|
||||
cleanart:
|
||||
rm -f art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
|
||||
@@ -88,19 +118,19 @@ art/mgmt_logo_white_wide.png: art/mgmt_logo_white_wide.svg
|
||||
all: docs $(PROGRAM).static
|
||||
|
||||
# show the current version
|
||||
version:
|
||||
version: ## show the current version
|
||||
@echo $(VERSION)
|
||||
|
||||
program:
|
||||
program: ## show the program name
|
||||
@echo $(PROGRAM)
|
||||
|
||||
path:
|
||||
path: ## create working paths
|
||||
./misc/make-path.sh
|
||||
|
||||
deps:
|
||||
deps: ## install system and golang dependencies
|
||||
./misc/make-deps.sh
|
||||
|
||||
run:
|
||||
run: ## run mgmt
|
||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
|
||||
# include race flag
|
||||
@@ -108,28 +138,27 @@ race:
|
||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
|
||||
# generate go files from non-go source
|
||||
bindata:
|
||||
@echo "Generating: bindata..."
|
||||
bindata: ## generate go files from non-go sources
|
||||
$(MAKE) --quiet -C bindata
|
||||
$(MAKE) --quiet -C lang/funcs
|
||||
|
||||
generate:
|
||||
go generate
|
||||
|
||||
lang:
|
||||
lang: ## generates the lexer/parser for the language frontend
|
||||
@# recursively run make in child dir named lang
|
||||
@echo "Generating: lang..."
|
||||
$(MAKE) --quiet -C lang
|
||||
@$(MAKE) --quiet -C lang
|
||||
|
||||
# build a `mgmt` binary for current host os/arch
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH}
|
||||
cp $< $@
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
||||
cp -a $< $@
|
||||
|
||||
$(PROGRAM).static: $(GO_FILES)
|
||||
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES)
|
||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||
go generate
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
|
||||
|
||||
build: LDFLAGS=-s -w
|
||||
build: LDFLAGS=-s -w ## build a fresh mgmt binary
|
||||
build: $(PROGRAM)
|
||||
|
||||
build-debug: LDFLAGS=
|
||||
@@ -139,25 +168,31 @@ build-debug: $(PROGRAM)
|
||||
# extract os and arch from target pattern
|
||||
GOOS=$(firstword $(subst -, ,$*))
|
||||
GOARCH=$(lastword $(subst -, ,$*))
|
||||
build/mgmt-%: $(GO_FILES) | bindata lang
|
||||
build/mgmt-%: $(GO_FILES) $(MCL_FILES) | bindata lang funcgen
|
||||
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
||||
@# reassigning GOOS and GOARCH to make build command copy/pastable
|
||||
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS);
|
||||
@time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS)
|
||||
|
||||
# create a list of binary file names to use as make targets
|
||||
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||
crossbuild: ${crossbuild_targets}
|
||||
|
||||
clean:
|
||||
clean: ## clean things up
|
||||
$(MAKE) --quiet -C test clean
|
||||
$(MAKE) --quiet -C bindata clean
|
||||
$(MAKE) --quiet -C lang/funcs clean
|
||||
$(MAKE) --quiet -C lang clean
|
||||
$(MAKE) --quiet -C misc/mkosi clean
|
||||
rm -f lang/funcs/core/generated_funcs.go || true
|
||||
rm -f lang/funcs/core/generated_funcs_test.go || true
|
||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||
rm -f *_stringer.go # generated by `go generate`
|
||||
rm -f *_mock.go # generated by `go generate`
|
||||
# crossbuild artifacts
|
||||
rm -f build/mgmt-*
|
||||
|
||||
test: build
|
||||
test: build ## run tests
|
||||
@# recursively run make in child dir named test
|
||||
@$(MAKE) --quiet -C test
|
||||
./test.sh
|
||||
|
||||
# create all test targets for make tab completion (eg: make test-gofmt)
|
||||
@@ -173,15 +208,15 @@ $(addprefix test-shell-,${test_shell}): test-shell-%: build
|
||||
|
||||
gofmt:
|
||||
# TODO: remove gofmt once goimports has a -s option
|
||||
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \;
|
||||
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \;
|
||||
find . -maxdepth 9 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \;
|
||||
find . -maxdepth 9 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \;
|
||||
|
||||
yamlfmt:
|
||||
find . -maxdepth 3 -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \;
|
||||
|
||||
format: gofmt yamlfmt
|
||||
format: gofmt yamlfmt ## format yaml and golang code
|
||||
|
||||
docs: $(PROGRAM)-documentation.pdf
|
||||
docs: $(PROGRAM)-documentation.pdf ## generate docs
|
||||
|
||||
$(PROGRAM)-documentation.pdf: docs/documentation.md
|
||||
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
|
||||
@@ -206,7 +241,7 @@ rpmbuild/SOURCES/: tar
|
||||
rpmbuild/SRPMS/: srpm
|
||||
rpmbuild/RPMS/: rpm
|
||||
|
||||
upload: upload-sources upload-srpms upload-rpms
|
||||
upload: upload-sources upload-srpms upload-rpms ## upload sources
|
||||
# do nothing
|
||||
|
||||
#
|
||||
@@ -311,33 +346,165 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
|
||||
rsync -avz --prune-empty-dirs rpmbuild/RPMS/ $(SERVER):$(REMOTE_PATH)/RPMS/; \
|
||||
fi
|
||||
|
||||
upload-releases:
|
||||
echo Running releases/ upload...
|
||||
rsync -avz --exclude '.mkdir' --exclude 'mgmt-release.url' releases/ $(SERVER):$(REMOTE_PATH)/releases/
|
||||
|
||||
#
|
||||
# copr build
|
||||
#
|
||||
copr: upload-srpms
|
||||
copr: upload-srpms ## build in copr
|
||||
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
|
||||
|
||||
#
|
||||
# deb build
|
||||
# tag
|
||||
#
|
||||
tag: ## tags a new release
|
||||
./misc/tag.sh
|
||||
|
||||
deb:
|
||||
./misc/gen-deb-changelog-from-git.sh
|
||||
dpkg-buildpackage
|
||||
# especially when building in Docker container, pull build artifact in project directory.
|
||||
cp ../mgmt_*_amd64.deb ./
|
||||
# cleanup
|
||||
rm -rf debian/mgmt/
|
||||
#
|
||||
# mkosi
|
||||
#
|
||||
mkosi: mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux ## builds distro packages via mkosi
|
||||
|
||||
build_container:
|
||||
mkosi_fedora-30: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
mkosi_fedora-29: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
mkosi_centos-7: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
mkosi_debian-10: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
mkosi_ubuntu-bionic: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
mkosi_archlinux: releases/$(VERSION)/.mkdir
|
||||
@title='$@' ; echo "Generating: $${title#'mkosi_'} via mkosi..."
|
||||
@title='$@' ; distro=$${title#'mkosi_'} ; ./misc/mkosi/make.sh $${distro} `realpath "releases/$(VERSION)/"`
|
||||
|
||||
#
|
||||
# release
|
||||
#
|
||||
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
|
||||
|
||||
releases_path:
|
||||
@#Don't put any other output or dependencies in here or they'll show!
|
||||
@echo "releases/$(VERSION)/"
|
||||
|
||||
release_fedora-30: $(PKG_FEDORA-30)
|
||||
release_fedora-29: $(PKG_FEDORA-29)
|
||||
release_centos-7: $(PKG_CENTOS-7)
|
||||
release_debian-10: $(PKG_DEBIAN-10)
|
||||
release_ubuntu-bionic: $(PKG_UBUNTU-BIONIC)
|
||||
release_archlinux: $(PKG_ARCHLINUX)
|
||||
|
||||
releases/$(VERSION)/mgmt-release.url: $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) $(SHA256SUMS_ASC)
|
||||
@echo "Pushing git tag $(VERSION) to origin..."
|
||||
git push origin $(VERSION)
|
||||
@echo "Creating github release..."
|
||||
hub release create \
|
||||
-F <( echo -e "$(VERSION)\n";echo "Verify the signatures of all packages before you use them. The signing key can be downloaded from https://purpleidea.com/contact/#pgp-key to verify the release." ) \
|
||||
-a $(PKG_FEDORA-30) \
|
||||
-a $(PKG_FEDORA-29) \
|
||||
-a $(PKG_CENTOS-7) \
|
||||
-a $(PKG_DEBIAN-10) \
|
||||
-a $(PKG_UBUNTU-BIONIC) \
|
||||
-a $(PKG_ARCHLINUX) \
|
||||
-a $(SHA256SUMS_ASC) \
|
||||
$(VERSION) \
|
||||
> releases/$(VERSION)/mgmt-release.url \
|
||||
&& cat releases/$(VERSION)/mgmt-release.url \
|
||||
|| rm -f releases/$(VERSION)/mgmt-release.url
|
||||
|
||||
releases/$(VERSION)/.mkdir:
|
||||
mkdir -p releases/$(VERSION)/{$(TOKEN_FEDORA-30),$(TOKEN_FEDORA-29),$(TOKEN_CENTOS-7),$(TOKEN_DEBIAN-10),$(TOKEN_UBUNTU-BIONIC),$(TOKEN_ARCHLINUX)}/ && touch releases/$(VERSION)/.mkdir
|
||||
|
||||
releases/$(VERSION)/$(TOKEN_FEDORA-30)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||
|
||||
$(PKG_FEDORA-30): releases/$(VERSION)/$(TOKEN_FEDORA-30)/changelog
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_FEDORA-30)" libvirt-devel augeas-devel
|
||||
|
||||
releases/$(VERSION)/$(TOKEN_FEDORA-29)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||
|
||||
$(PKG_FEDORA-29): releases/$(VERSION)/$(TOKEN_FEDORA-29)/changelog
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_FEDORA-29)" libvirt-devel augeas-devel
|
||||
|
||||
releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-rpm-changelog.sh "$${distro}" $(VERSION)
|
||||
|
||||
$(PKG_CENTOS-7): releases/$(VERSION)/$(TOKEN_CENTOS-7)/changelog
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_CENTOS-7)" libvirt-devel augeas-devel
|
||||
|
||||
releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-deb-changelog.sh "$${distro}" $(VERSION)
|
||||
|
||||
$(PKG_DEBIAN-10): releases/$(VERSION)/$(TOKEN_DEBIAN-10)/changelog
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_DEBIAN-10)" libvirt-dev libaugeas-dev
|
||||
|
||||
releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Generating: $${distro} changelog..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/make-deb-changelog.sh "$${distro}" $(VERSION)
|
||||
|
||||
$(PKG_UBUNTU-BIONIC): releases/$(VERSION)/$(TOKEN_UBUNTU-BIONIC)/changelog
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_UBUNTU-BIONIC)" libvirt-dev libaugeas-dev
|
||||
|
||||
$(PKG_ARCHLINUX): $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; echo "Building: $${distro} package..."
|
||||
@title='$(@D)' ; distro=$${title#'releases/$(VERSION)/'} ; ./misc/fpm-pack.sh $${distro} $(VERSION) "$(FILE_ARCHLINUX)" libvirt augeas
|
||||
|
||||
$(SHA256SUMS): $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX)
|
||||
@# remove the directory separator in the SHA256SUMS file
|
||||
@echo "Generating: sha256 sum..."
|
||||
sha256sum $(PKG_FEDORA-30) $(PKG_FEDORA-29) $(PKG_CENTOS-7) $(PKG_DEBIAN-10) $(PKG_UBUNTU-BIONIC) $(PKG_ARCHLINUX) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
|
||||
|
||||
$(SHA256SUMS_ASC): $(SHA256SUMS)
|
||||
@echo "Signing sha256 sum..."
|
||||
gpg2 --yes --clearsign $(SHA256SUMS)
|
||||
|
||||
build_container: ## builds the container
|
||||
docker build -t purpleidea/mgmt-build -f docker/Dockerfile.build .
|
||||
docker run -td --name mgmt-build purpleidea/mgmt-build
|
||||
docker cp mgmt-build:/root/gopath/src/github.com/purpleidea/mgmt/mgmt .
|
||||
docker build -t purpleidea/mgmt -f docker/Dockerfile.static .
|
||||
docker rm mgmt-build || true
|
||||
|
||||
clean_container:
|
||||
clean_container: ## removes the container
|
||||
docker rmi purpleidea/mgmt-build
|
||||
docker rmi purpleidea/mgmt
|
||||
|
||||
help: ## show this help screen
|
||||
@echo 'Usage: make <OPTIONS> ... <TARGETS>'
|
||||
@echo ''
|
||||
@echo 'Available targets are:'
|
||||
@echo ''
|
||||
@grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
|
||||
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ''
|
||||
|
||||
funcgen: lang/funcs/core/generated_funcs.go
|
||||
|
||||
lang/funcs/core/generated_funcs.go: lang/funcs/funcgen/*.go lang/funcs/core/funcgen.yaml lang/funcs/funcgen/templates/generated_funcs.go.tpl
|
||||
@echo "Generating: funcs..."
|
||||
@go run `find lang/funcs/funcgen/ -maxdepth 1 -type f -name '*.go' -not -name '*_test.go'` -templates=lang/funcs/funcgen/templates/generated_funcs.go.tpl >/dev/null
|
||||
|
||||
# vim: ts=8
|
||||
|
||||
76
README.md
76
README.md
@@ -4,9 +4,61 @@
|
||||
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](https://github.com/purpleidea/mgmt/actions/)
|
||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
[](https://web.libera.chat/?channels=#mgmtconfig)
|
||||
[](https://www.patreon.com/purpleidea)
|
||||
[](https://liberapay.com/purpleidea/donate)
|
||||
|
||||
## About:
|
||||
|
||||
`Mgmt` is a real-time automation tool. It is familiar to existing configuration
|
||||
management software, but is drastically more powerful as it can allow you to
|
||||
build real-time, closed-loop feedback systems, in a very safe way, and with a
|
||||
surprisingly small amout of our `mcl` code. For example, the following code will
|
||||
ensure that your file server is set to read-only when it's friday.
|
||||
|
||||
```mcl
|
||||
import "datetime"
|
||||
$is_friday = datetime.weekday(datetime.now()) == "friday"
|
||||
file "/srv/files/" {
|
||||
state => $const.res.file.state.exists,
|
||||
mode => if $is_friday { # this updates the mode, the instant it changes!
|
||||
"0550"
|
||||
} else {
|
||||
"0770"
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
It can run continuously, intermittently, or on-demand, and in the first case, it
|
||||
will guarantee that your system is always in the desired state for that instant!
|
||||
In this mode it can run as a decentralized cluster of agents across your
|
||||
network, each exchanging information with the others in real-time, to respond to
|
||||
your changing needs. For example, if you want to ensure that some resource runs
|
||||
on a maximum of two hosts in your cluster, you can specify that as well:
|
||||
|
||||
```mcl
|
||||
import "sys"
|
||||
import "world"
|
||||
|
||||
# we'll set a few scheduling options:
|
||||
$opts = struct{strategy => "rr", max => 2, ttl => 10,}
|
||||
|
||||
# schedule in a particular namespace with options:
|
||||
$set = world.schedule("xsched", $opts)
|
||||
|
||||
if sys.hostname() in $set {
|
||||
# use your imagination to put something more complex right here...
|
||||
print "i got scheduled" {} # this will run on the chosen machines
|
||||
}
|
||||
```
|
||||
|
||||
As you add and remove hosts from the cluster, the real-time `schedule` function
|
||||
will dynamically pick up to two hosts from the available pool. These specific
|
||||
functions aren't intrinsic to the core design, and new ones can be easily added.
|
||||
|
||||
Please read on if you'd like to learn more...
|
||||
|
||||
## Community:
|
||||
|
||||
@@ -14,9 +66,11 @@ Come join us in the `mgmt` community!
|
||||
|
||||
| Medium | Link |
|
||||
|---|---|
|
||||
| IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
|
||||
| IRC | [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig) on Libera.Chat |
|
||||
| Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) |
|
||||
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
|
||||
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |
|
||||
| Liberapay | [purpleidea](https://liberapay.com/purpleidea/donate) on Liberapay |
|
||||
|
||||
## Status:
|
||||
|
||||
@@ -27,7 +81,7 @@ approach. The project contains an engine and a language.
|
||||
|
||||
Mgmt is a fairly new project. It is usable today, but not yet feature complete.
|
||||
With your help you'll be able to influence our design and get us to 1.0 sooner!
|
||||
Interested developers should read the [quick start guide](docs/quick-start-guide.md).
|
||||
Interested users should read the [quick start guide](docs/quick-start-guide.md).
|
||||
|
||||
## Documentation:
|
||||
|
||||
@@ -35,7 +89,7 @@ Please read, enjoy and help improve our documentation!
|
||||
|
||||
| Documentation | Additional Notes |
|
||||
|---|---|
|
||||
| [quick start guide](docs/quick-start-guide.md) | for mgmt developers |
|
||||
| [quick start guide](docs/quick-start-guide.md) | for everyone |
|
||||
| [frequently asked questions](docs/faq.md) | for everyone |
|
||||
| [general documentation](docs/documentation.md) | for everyone |
|
||||
| [language guide](docs/language-guide.md) | for everyone |
|
||||
@@ -54,22 +108,18 @@ If you have a well phrased question that might benefit others, consider asking
|
||||
it by sending a patch to the [FAQ](docs/faq.md) section. I'll merge your
|
||||
question, and a patch with the answer!
|
||||
|
||||
## Roadmap:
|
||||
## Get involved:
|
||||
|
||||
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
issues if you're a first time contributor to the project or if you're unsure
|
||||
about what to hack on!
|
||||
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
|
||||
Please get involved by working on one of these items or by suggesting something
|
||||
else!
|
||||
about what to hack on! Please get involved by working on one of these items or
|
||||
by suggesting something else! There are some lower priority issues and harder
|
||||
issues available in our [TODO](TODO.md) file. Please have a look.
|
||||
|
||||
## Bugs:
|
||||
|
||||
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go)
|
||||
to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
|
||||
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell)
|
||||
or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible
|
||||
test case.
|
||||
Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/).
|
||||
|
||||
## Patches:
|
||||
|
||||
65
TODO.md
65
TODO.md
@@ -1,10 +1,18 @@
|
||||
# TODO
|
||||
|
||||
If you're looking for something to do, look here!
|
||||
Let us know if you're working on one of the items.
|
||||
If you'd like something to work on, ping @purpleidea and I'll create an issue
|
||||
tailored especially for you! Just let me know your approximate golang skill
|
||||
level and how many hours you'd like to spend on the patch.
|
||||
Here is a TODO list of longstanding items that are either lower-priority, or
|
||||
more involved in terms of time, skill-level, and/or motivation.
|
||||
|
||||
Please have a look, and let us know if you're working on one of the items. It's
|
||||
best to open an issue to track your progress and to discuss any implementation
|
||||
questions you might have.
|
||||
|
||||
Lastly, if you'd like something different to work on, please ping @purpleidea
|
||||
and I'll create an issue tailored especially for your approximate golang skill
|
||||
level and available time commitment in terms of hours you'd need to spend on the
|
||||
patch.
|
||||
|
||||
Happy Hacking!
|
||||
|
||||
## Package resource
|
||||
|
||||
@@ -19,7 +27,7 @@ level and how many hours you'd like to spend on the patch.
|
||||
|
||||
## Svc resource
|
||||
|
||||
- [ ] base resource improvements
|
||||
- [ ] refreshonly support [:heart:](https://github.com/purpleidea/mgmt/issues/464)
|
||||
|
||||
## Exec resource
|
||||
|
||||
@@ -33,33 +41,14 @@ level and how many hours you'd like to spend on the patch.
|
||||
|
||||
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Virt (libvirt) resource
|
||||
|
||||
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Net (systemd-networkd) resource
|
||||
|
||||
- [ ] base resource
|
||||
|
||||
## Nspawn (systemd-nspawn) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Mount (systemd-mount) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Cron (systemd-timer) resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Http resource
|
||||
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Etcd improvements
|
||||
|
||||
- [ ] fix embedded etcd master race
|
||||
- [ ] fix etcd race bug that only happens during CI testing (intermittently
|
||||
failing test case issue)
|
||||
|
||||
## Torrent/dht file transfer
|
||||
|
||||
@@ -69,17 +58,33 @@ level and how many hours you'd like to spend on the patch.
|
||||
|
||||
- [ ] base plumbing
|
||||
|
||||
## Resource improvements
|
||||
|
||||
- [ ] more reversible resources implemented
|
||||
- [ ] more "cloud" resources
|
||||
|
||||
## Language improvements
|
||||
|
||||
- [ ] more core functions
|
||||
- [ ] automatic language formatter, ala `gofmt`
|
||||
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
||||
- [ ] vim syntax highlighting
|
||||
- [x] emacs syntax highlighting: see `misc/emacs/`
|
||||
- [ ] emacs syntax highlighting: see `misc/emacs/` (needs updating)
|
||||
- [ ] exposed $error variable for feedback in the language
|
||||
- [ ] improve the printf function to add %[]s, %[]f ([]str, []float) and map,
|
||||
struct, nested etc... %v would be nice too!
|
||||
- [ ] add line/col/file annotations to AST so we can get locations of errors
|
||||
that the parser finds
|
||||
- [ ] add more error messages with the `%error` pattern in parser.y
|
||||
- [ ] we should have helper functions or language sugar to pull a field out of a
|
||||
struct, or a value out of a map, or an index out of a list, etc...
|
||||
|
||||
## Engine improvements
|
||||
|
||||
- [ ] add a "waiting for func" message in the func engine to notify the user
|
||||
about slow functions...
|
||||
|
||||
## Other
|
||||
|
||||
- [ ] better error/retry handling
|
||||
- [ ] deb package target in Makefile
|
||||
- [ ] reproducible builds
|
||||
- [ ] add your suggestions!
|
||||
|
||||
11
Vagrantfile
vendored
11
Vagrantfile
vendored
@@ -6,7 +6,7 @@ Vagrant.configure(2) do |config|
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
config.vm.define "mgmt-dev" do |instance|
|
||||
instance.vm.box = "fedora/26-cloud-base"
|
||||
instance.vm.box = "bento/fedora-31"
|
||||
end
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
@@ -23,8 +23,7 @@ Vagrant.configure(2) do |config|
|
||||
config.vm.provision "file", source: "vagrant/mgmt.bashrc", destination: ".mgmt.bashrc"
|
||||
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
|
||||
|
||||
# copied from make-deps.sh (with added git)
|
||||
config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make"
|
||||
config.vm.provision "shell", inline: "dnf install -y golang git make"
|
||||
|
||||
# set up packagekit
|
||||
config.vm.provision "shell" do |shell|
|
||||
@@ -39,8 +38,10 @@ Vagrant.configure(2) do |config|
|
||||
script = <<-SCRIPT
|
||||
grep -q 'mgmt\.bashrc' ~/.bashrc || echo '. ~/.mgmt.bashrc' >>~/.bashrc
|
||||
. ~/.mgmt.bashrc
|
||||
go get -u github.com/purpleidea/mgmt
|
||||
cd ~/gopath/src/github.com/purpleidea/mgmt
|
||||
mkdir -p ~/gopath/src/github.com/purpleidea
|
||||
cd ~/gopath/src/github.com/purpleidea
|
||||
git clone https://github.com/purpleidea/mgmt --recursive
|
||||
cd mgmt
|
||||
make deps
|
||||
SCRIPT
|
||||
config.vm.provision "shell" do |shell|
|
||||
|
||||
BIN
art/mgmt.png
BIN
art/mgmt.png
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 683 KiB |
BIN
art/mgmt_poohbear_meme.jpg
Normal file
BIN
art/mgmt_poohbear_meme.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
@@ -1,5 +1,5 @@
|
||||
# Mgmt
|
||||
# Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
# Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
# Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -16,9 +16,12 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# The bindata target generates go files from any source defined below. To use
|
||||
# the files, import the "bindata" package and use:
|
||||
# the files, import the generated "bindata" package and use:
|
||||
# `bytes, err := bindata.Asset("FILEPATH")`
|
||||
# where FILEPATH is the path of the original input file relative to `bindata/`.
|
||||
# To get a list of files stored in this "bindata" package, you can use:
|
||||
# `paths := bindata.AssetNames()` and `paths, err := bindata.AssetDir(name)`
|
||||
# to get a list of files with a directory prefix.
|
||||
|
||||
.PHONY: build clean
|
||||
default: build
|
||||
@@ -27,6 +30,7 @@ build: bindata.go
|
||||
|
||||
# add more input files as dependencies at the end here...
|
||||
bindata.go: ../COPYING
|
||||
@echo "Generating: bindata..."
|
||||
# go-bindata --pkg bindata -o <OUTPUT> <INPUT>
|
||||
go-bindata --pkg bindata -o ./$@ $^
|
||||
# gofmt the output file
|
||||
@@ -34,5 +38,5 @@ bindata.go: ../COPYING
|
||||
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@'
|
||||
|
||||
clean:
|
||||
# remove generated bindata/*.go
|
||||
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f *.go
|
||||
# remove generated bindata.go
|
||||
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f bindata.go
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,135 +20,256 @@ package converger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// TODO: we could make a new function that masks out the state of certain
|
||||
// UID's, but at the moment the new Timer code has obsoleted the need...
|
||||
|
||||
// Converger is the general interface for implementing a convergence watcher.
|
||||
type Converger interface { // TODO: need a better name
|
||||
Register() UID
|
||||
IsConverged(UID) bool // is the UID converged ?
|
||||
SetConverged(UID, bool) error // set the converged state of the UID
|
||||
Unregister(UID)
|
||||
Start()
|
||||
Pause()
|
||||
Loop(bool)
|
||||
ConvergedTimer(UID) <-chan time.Time
|
||||
Status() map[uint64]bool
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
SetStateFn(func(bool) error) // sets the stateFn
|
||||
}
|
||||
|
||||
// UID is the interface resources can use to notify with if converged. You'll
|
||||
// need to use part of the Converger interface to Register initially too.
|
||||
type UID interface {
|
||||
ID() uint64 // get Id
|
||||
Name() string // get a friendly name
|
||||
SetName(string)
|
||||
IsValid() bool // has Id been initialized ?
|
||||
InvalidateID() // set Id to nil
|
||||
IsConverged() bool
|
||||
SetConverged(bool) error
|
||||
Unregister()
|
||||
ConvergedTimer() <-chan time.Time
|
||||
StartTimer() (func() error, error) // cancellable is the same as StopTimer()
|
||||
ResetTimer() error // resets counter to zero
|
||||
StopTimer() error
|
||||
}
|
||||
|
||||
// converger is an implementation of the Converger interface.
|
||||
type converger struct {
|
||||
timeout int // must be zero (instant) or greater seconds to run
|
||||
stateFn func(bool) error // run on converged state changes with state bool
|
||||
converged bool // did we converge (state changes of this run Fn)
|
||||
channel chan struct{} // signal here to run an isConverged check
|
||||
control chan bool // control channel for start/pause
|
||||
mutex sync.RWMutex // used for controlling access to status and lastid
|
||||
lastid uint64
|
||||
status map[uint64]bool
|
||||
}
|
||||
|
||||
// cuid is an implementation of the UID interface.
|
||||
type cuid struct {
|
||||
converger Converger
|
||||
id uint64
|
||||
name string // user defined, friendly name
|
||||
mutex sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the above timer running?
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewConverger builds a new converger struct.
|
||||
func NewConverger(timeout int, stateFn func(bool) error) Converger {
|
||||
return &converger{
|
||||
// New builds a new converger coordinator.
|
||||
func New(timeout int64) *Coordinator {
|
||||
return &Coordinator{
|
||||
timeout: timeout,
|
||||
stateFn: stateFn,
|
||||
channel: make(chan struct{}),
|
||||
control: make(chan bool),
|
||||
lastid: 0,
|
||||
status: make(map[uint64]bool),
|
||||
|
||||
mutex: &sync.RWMutex{},
|
||||
|
||||
//lastid: 0,
|
||||
status: make(map[*UID]struct{}),
|
||||
|
||||
//converged: false, // initial state
|
||||
|
||||
pokeChan: make(chan struct{}, 1), // must be buffered
|
||||
|
||||
readyChan: make(chan struct{}), // ready signal
|
||||
|
||||
//paused: false, // starts off as started
|
||||
pauseSignal: make(chan struct{}),
|
||||
//resumeSignal: make(chan struct{}), // happens on pause
|
||||
//pausedAck: util.NewEasyAck(), // happens on pause
|
||||
|
||||
stateFns: make(map[string]func(bool) error),
|
||||
smutex: &sync.RWMutex{},
|
||||
|
||||
closeChan: make(chan struct{}),
|
||||
wg: &sync.WaitGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
// Register assigns a UID to the caller.
|
||||
func (obj *converger) Register() UID {
|
||||
// Coordinator is the central converger engine.
|
||||
type Coordinator struct {
|
||||
// timeout must be zero (instant) or greater seconds to run. If it's -1
|
||||
// then this is disabled, and we never run stateFns.
|
||||
timeout int64
|
||||
|
||||
// mutex is used for controlling access to status and lastid.
|
||||
mutex *sync.RWMutex
|
||||
|
||||
// lastid contains the last uid we used for registration.
|
||||
//lastid uint64
|
||||
// status contains a reference to each active UID.
|
||||
status map[*UID]struct{}
|
||||
|
||||
// converged stores the last convergence state. When this changes, we
|
||||
// run the stateFns.
|
||||
converged bool
|
||||
|
||||
// pokeChan receives a message every time we might need to re-calculate.
|
||||
pokeChan chan struct{}
|
||||
|
||||
// readyChan closes to notify any interested parties that the main loop
|
||||
// is running.
|
||||
readyChan chan struct{}
|
||||
|
||||
// paused represents if this coordinator is paused or not.
|
||||
paused bool
|
||||
// pauseSignal closes to request a pause of this coordinator.
|
||||
pauseSignal chan struct{}
|
||||
// resumeSignal closes to request a resume of this coordinator.
|
||||
resumeSignal chan struct{}
|
||||
// pausedAck is used to send an ack message saying that we've paused.
|
||||
pausedAck *util.EasyAck
|
||||
|
||||
// stateFns run on converged state changes.
|
||||
stateFns map[string]func(bool) error
|
||||
// smutex is used for controlling access to the stateFns map.
|
||||
smutex *sync.RWMutex
|
||||
|
||||
// closeChan closes when we've been requested to shutdown.
|
||||
closeChan chan struct{}
|
||||
// wg waits for everything to finish.
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Register creates a new UID which can be used to report converged state. You
|
||||
// must Unregister each UID before Shutdown will be able to finish running.
|
||||
func (obj *Coordinator) Register() *UID {
|
||||
obj.wg.Add(1) // additional tracking for each UID
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
obj.lastid++
|
||||
obj.status[obj.lastid] = false // initialize as not converged
|
||||
return &cuid{
|
||||
converger: obj,
|
||||
id: obj.lastid,
|
||||
name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||
timer: nil,
|
||||
running: false,
|
||||
//obj.lastid++
|
||||
uid := &UID{
|
||||
timeout: obj.timeout, // copy the timeout here
|
||||
//id: obj.lastid,
|
||||
//name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||
|
||||
poke: obj.poke,
|
||||
|
||||
// timer
|
||||
mutex: &sync.Mutex{},
|
||||
timer: nil,
|
||||
running: false,
|
||||
wg: &sync.WaitGroup{},
|
||||
}
|
||||
uid.unregister = func() { obj.Unregister(uid) } // add unregister func
|
||||
obj.status[uid] = struct{}{} // TODO: add converged state here?
|
||||
return uid
|
||||
}
|
||||
|
||||
// IsConverged gets the converged status of a uid.
|
||||
func (obj *converger) IsConverged(uid UID) bool {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||
}
|
||||
obj.mutex.RLock()
|
||||
isConverged, found := obj.status[uid.ID()] // lookup
|
||||
obj.mutex.RUnlock()
|
||||
if !found {
|
||||
panic("the ID of UID is unregistered")
|
||||
}
|
||||
return isConverged
|
||||
}
|
||||
|
||||
// SetConverged updates the converger with the converged state of the UID.
|
||||
func (obj *converger) SetConverged(uid UID, isConverged bool) error {
|
||||
if !uid.IsValid() {
|
||||
return fmt.Errorf("the ID of UID(%s) is nil", uid.Name())
|
||||
}
|
||||
// Unregister removes the UID from the converger coordinator. If you supply an
|
||||
// invalid or unregistered uid to this function, it will panic. An unregistered
|
||||
// UID is no longer part of the convergence checking.
|
||||
func (obj *Coordinator) Unregister(uid *UID) {
|
||||
defer obj.wg.Done() // additional tracking for each UID
|
||||
obj.mutex.Lock()
|
||||
if _, found := obj.status[uid.ID()]; !found {
|
||||
panic("the ID of UID is unregistered")
|
||||
defer obj.mutex.Unlock()
|
||||
|
||||
if _, exists := obj.status[uid]; !exists {
|
||||
panic("uid is not registered")
|
||||
}
|
||||
obj.status[uid.ID()] = isConverged // set
|
||||
obj.mutex.Unlock() // unlock *before* poke or deadlock!
|
||||
if isConverged != obj.converged { // only poke if it would be helpful
|
||||
// run in a go routine so that we never block... just queue up!
|
||||
// this allows us to send events, even if we haven't started...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
uid.StopTimer() // ignore any errors
|
||||
delete(obj.status, uid)
|
||||
}
|
||||
|
||||
// Run starts the main loop for the converger coordinator. It is commonly run
|
||||
// from a go routine. It blocks until the Shutdown method is run to close it.
|
||||
// NOTE: when we have very short timeouts, if we start before all the resources
|
||||
// have joined the map, then it might appear as if we converged before we did!
|
||||
func (obj *Coordinator) Run(startPaused bool) {
|
||||
obj.wg.Add(1)
|
||||
wg := &sync.WaitGroup{} // needed for the startPaused
|
||||
defer wg.Wait() // don't leave any leftover go routines running
|
||||
if startPaused {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
obj.Pause() // ignore any errors
|
||||
close(obj.readyChan)
|
||||
}()
|
||||
} else {
|
||||
close(obj.readyChan) // we must wait till the wg.Add(1) has happened...
|
||||
}
|
||||
defer obj.wg.Done()
|
||||
for {
|
||||
// pause if one was requested...
|
||||
select {
|
||||
case <-obj.pauseSignal: // channel closes
|
||||
obj.pausedAck.Ack() // send ack
|
||||
// we are paused now, and waiting for resume or exit...
|
||||
select {
|
||||
case <-obj.resumeSignal: // channel closes
|
||||
// resumed!
|
||||
|
||||
case <-obj.closeChan: // we can always escape
|
||||
return
|
||||
}
|
||||
|
||||
case _, ok := <-obj.pokeChan: // we got an event (re-calculate)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := obj.test(); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
|
||||
case <-obj.closeChan: // we can always escape
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ready blocks until the Run loop has started up. This is useful so that we
|
||||
// don't run Shutdown before we've even started up properly.
|
||||
func (obj *Coordinator) Ready() {
|
||||
select {
|
||||
case <-obj.readyChan:
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown sends a signal to the Run loop that it should exit. This blocks
|
||||
// until it does.
|
||||
func (obj *Coordinator) Shutdown() {
|
||||
close(obj.closeChan)
|
||||
obj.wg.Wait()
|
||||
close(obj.pokeChan) // free memory?
|
||||
}
|
||||
|
||||
// Pause pauses the coordinator. It should not be called on an already paused
|
||||
// coordinator. It will block until the coordinator pauses with an
|
||||
// acknowledgment, or until an exit is requested. If the latter happens it will
|
||||
// error. It is NOT thread-safe with the Resume() method so only call either one
|
||||
// at a time.
|
||||
func (obj *Coordinator) Pause() error {
|
||||
if obj.paused {
|
||||
return fmt.Errorf("already paused")
|
||||
}
|
||||
|
||||
obj.pausedAck = util.NewEasyAck()
|
||||
obj.resumeSignal = make(chan struct{}) // build the resume signal
|
||||
close(obj.pauseSignal)
|
||||
|
||||
// wait for ack (or exit signal)
|
||||
select {
|
||||
case <-obj.pausedAck.Wait(): // we got it!
|
||||
// we're paused
|
||||
case <-obj.closeChan:
|
||||
return fmt.Errorf("closing")
|
||||
}
|
||||
obj.paused = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isConverged returns true if *every* registered uid has converged.
|
||||
func (obj *converger) isConverged() bool {
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for _, v := range obj.status {
|
||||
// Resume unpauses the coordinator. It can be safely called on a brand-new
|
||||
// coordinator that has just started running without incident. It is NOT
|
||||
// thread-safe with the Pause() method, so only call either one at a time.
|
||||
func (obj *Coordinator) Resume() {
|
||||
// TODO: do we need a mutex around Resume?
|
||||
if !obj.paused { // no need to unpause brand-new resources
|
||||
return
|
||||
}
|
||||
|
||||
obj.pauseSignal = make(chan struct{}) // rebuild for next pause
|
||||
close(obj.resumeSignal)
|
||||
obj.poke() // unblock and notice the resume if necessary
|
||||
|
||||
obj.paused = false
|
||||
|
||||
// no need to wait for it to resume
|
||||
//return // implied
|
||||
}
|
||||
|
||||
// poke sends a message to the coordinator telling it that it should re-evaluate
|
||||
// whether we're converged or not. This does not block. Do not run this in a
|
||||
// goroutine. It must not be called after Shutdown has been called.
|
||||
func (obj *Coordinator) poke() {
|
||||
// redundant
|
||||
//if len(obj.pokeChan) > 0 {
|
||||
// return
|
||||
//}
|
||||
|
||||
select {
|
||||
case obj.pokeChan <- struct{}{}:
|
||||
default: // if chan is now full because more than one poke happened...
|
||||
}
|
||||
}
|
||||
|
||||
// IsConverged returns true if *every* registered uid has converged. If there
|
||||
// are no registered UID's, then this will return true.
|
||||
func (obj *Coordinator) IsConverged() bool {
|
||||
for _, v := range obj.Status() {
|
||||
if !v { // everyone must be converged for this to be true
|
||||
return false
|
||||
}
|
||||
@@ -156,194 +277,170 @@ func (obj *converger) isConverged() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Unregister dissociates the ConvergedUID from the converged checking.
|
||||
func (obj *converger) Unregister(uid UID) {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||
// test evaluates whether we're converged or not and runs the state change. It
|
||||
// is NOT thread-safe.
|
||||
func (obj *Coordinator) test() error {
|
||||
// TODO: add these checks elsewhere to prevent anything from running?
|
||||
if obj.timeout < 0 {
|
||||
return nil // nothing to do (only run if timeout is valid)
|
||||
}
|
||||
obj.mutex.Lock()
|
||||
uid.StopTimer() // ignore any errors
|
||||
delete(obj.status, uid.ID())
|
||||
obj.mutex.Unlock()
|
||||
uid.InvalidateID()
|
||||
}
|
||||
|
||||
// Start causes a Converger object to start or resume running.
|
||||
func (obj *converger) Start() {
|
||||
obj.control <- true
|
||||
}
|
||||
converged := obj.IsConverged()
|
||||
defer func() {
|
||||
obj.converged = converged // set this only at the end...
|
||||
}()
|
||||
|
||||
// Pause causes a Converger object to stop running temporarily.
|
||||
func (obj *converger) Pause() { // FIXME: add a sync ACK on pause before return
|
||||
obj.control <- false
|
||||
}
|
||||
|
||||
// Loop is the main loop for a Converger object. It usually runs in a goroutine.
|
||||
// TODO: we could eventually have each resource tell us as soon as it converges,
|
||||
// and then keep track of the time delays here, to avoid callers needing select.
|
||||
// NOTE: when we have very short timeouts, if we start before all the resources
|
||||
// have joined the map, then it might appear as if we converged before we did!
|
||||
func (obj *converger) Loop(startPaused bool) {
|
||||
if obj.control == nil {
|
||||
panic("converger not initialized correctly")
|
||||
}
|
||||
if startPaused { // start paused without racing
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("converger expected true")
|
||||
}
|
||||
if !converged {
|
||||
if !obj.converged { // were we previously also not converged?
|
||||
return nil // nothing to do
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case e := <-obj.control: // expecting "false" which means pause!
|
||||
if e {
|
||||
panic("converger expected false")
|
||||
}
|
||||
// now i'm paused...
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("converger expected true")
|
||||
}
|
||||
// restart
|
||||
// kick once to refresh the check...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
continue
|
||||
}
|
||||
|
||||
case <-obj.channel:
|
||||
if !obj.isConverged() {
|
||||
if obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(false); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = false
|
||||
continue
|
||||
}
|
||||
|
||||
// we have converged!
|
||||
if obj.timeout >= 0 { // only run if timeout is valid
|
||||
if !obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(true); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = true
|
||||
// loop and wait again...
|
||||
}
|
||||
// we're doing a state change
|
||||
// call the arbitrary functions (takes a read lock!)
|
||||
return obj.runStateFns(false)
|
||||
}
|
||||
|
||||
// we have converged!
|
||||
if obj.converged { // were we previously also converged?
|
||||
return nil // nothing to do
|
||||
}
|
||||
|
||||
// call the arbitrary functions (takes a read lock!)
|
||||
return obj.runStateFns(true)
|
||||
}
|
||||
|
||||
// ConvergedTimer adds a timeout to a select call and blocks until then.
|
||||
// TODO: this means we could eventually have per resource converged timeouts
|
||||
func (obj *converger) ConvergedTimer(uid UID) <-chan time.Time {
|
||||
// be clever: if i'm already converged, this timeout should block which
|
||||
// avoids unnecessary new signals being sent! this avoids fast loops if
|
||||
// we have a low timeout, or in particular a timeout == 0
|
||||
if uid.IsConverged() {
|
||||
// blocks the case statement in select forever!
|
||||
return util.TimeAfterOrBlock(-1)
|
||||
// runStateFns runs the list of stored state functions.
|
||||
func (obj *Coordinator) runStateFns(converged bool) error {
|
||||
obj.smutex.RLock()
|
||||
defer obj.smutex.RUnlock()
|
||||
var keys []string
|
||||
for k := range obj.stateFns {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return util.TimeAfterOrBlock(obj.timeout)
|
||||
sort.Strings(keys)
|
||||
var err error
|
||||
for _, name := range keys { // run in deterministic order
|
||||
fn := obj.stateFns[name]
|
||||
// call an arbitrary function
|
||||
e := fn(converged)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// AddStateFn adds a state function to be run on change of converged state.
|
||||
func (obj *Coordinator) AddStateFn(name string, stateFn func(bool) error) error {
|
||||
obj.smutex.Lock()
|
||||
defer obj.smutex.Unlock()
|
||||
if _, exists := obj.stateFns[name]; exists {
|
||||
return fmt.Errorf("a stateFn with that name already exists")
|
||||
}
|
||||
obj.stateFns[name] = stateFn
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveStateFn removes a state function from running on change of converged
|
||||
// state.
|
||||
func (obj *Coordinator) RemoveStateFn(name string) error {
|
||||
obj.smutex.Lock()
|
||||
defer obj.smutex.Unlock()
|
||||
if _, exists := obj.stateFns[name]; !exists {
|
||||
return fmt.Errorf("a stateFn with that name doesn't exist")
|
||||
}
|
||||
delete(obj.stateFns, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns a map of the converged status of each UID.
|
||||
func (obj *converger) Status() map[uint64]bool {
|
||||
status := make(map[uint64]bool)
|
||||
func (obj *Coordinator) Status() map[*UID]bool {
|
||||
status := make(map[*UID]bool)
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for k, v := range obj.status { // make a copy to avoid the mutex
|
||||
status[k] = v
|
||||
for k := range obj.status {
|
||||
status[k] = k.IsConverged()
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// Timeout returns the timeout in seconds that converger was created with. This
|
||||
// is useful to avoid passing in the timeout value separately when you're
|
||||
// already passing in the Converger struct.
|
||||
func (obj *converger) Timeout() int {
|
||||
// already passing in the Coordinator struct.
|
||||
func (obj *Coordinator) Timeout() int64 {
|
||||
return obj.timeout
|
||||
}
|
||||
|
||||
// SetStateFn sets the state function to be run on change of converged state.
|
||||
func (obj *converger) SetStateFn(stateFn func(bool) error) {
|
||||
obj.stateFn = stateFn
|
||||
// UID represents one of the probes for the converger coordinator. It is created
|
||||
// by calling the Register method of the Coordinator struct. It should be freed
|
||||
// after use with Unregister.
|
||||
type UID struct {
|
||||
// timeout is a copy of the main timeout. It could eventually be used
|
||||
// for per-UID timeouts too.
|
||||
timeout int64
|
||||
// isConverged stores the convergence state of this particular UID.
|
||||
isConverged bool
|
||||
|
||||
// poke stores a reference to the main poke function.
|
||||
poke func()
|
||||
// unregister stores a reference to the unregister function.
|
||||
unregister func()
|
||||
|
||||
// timer
|
||||
mutex *sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the timer running?
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// ID returns the unique id of this UID object.
|
||||
func (obj *cuid) ID() uint64 {
|
||||
return obj.id
|
||||
// Unregister removes this UID from the converger coordinator. An unregistered
|
||||
// UID is no longer part of the convergence checking.
|
||||
func (obj *UID) Unregister() {
|
||||
obj.unregister()
|
||||
}
|
||||
|
||||
// Name returns a user defined name for the specific cuid.
|
||||
func (obj *cuid) Name() string {
|
||||
return obj.name
|
||||
// IsConverged reports whether this UID is converged or not.
|
||||
func (obj *UID) IsConverged() bool {
|
||||
return obj.isConverged
|
||||
}
|
||||
|
||||
// SetName sets a user defined name for the specific cuid.
|
||||
func (obj *cuid) SetName(name string) {
|
||||
obj.name = name
|
||||
// SetConverged sets the convergence state of this UID. This is used by the
|
||||
// running timer if one is started. The timer will overwrite any value set by
|
||||
// this method.
|
||||
func (obj *UID) SetConverged(isConverged bool) {
|
||||
obj.isConverged = isConverged
|
||||
obj.poke() // notify of change
|
||||
}
|
||||
|
||||
// IsValid tells us if the id is valid or has already been destroyed.
|
||||
func (obj *cuid) IsValid() bool {
|
||||
return obj.id != 0 // an id of 0 is invalid
|
||||
// ConvergedTimer adds a timeout to a select call and blocks until then.
|
||||
// TODO: this means we could eventually have per resource converged timeouts
|
||||
func (obj *UID) ConvergedTimer() <-chan time.Time {
|
||||
// be clever: if i'm already converged, this timeout should block which
|
||||
// avoids unnecessary new signals being sent! this avoids fast loops if
|
||||
// we have a low timeout, or in particular a timeout == 0
|
||||
if obj.IsConverged() {
|
||||
// blocks the case statement in select forever!
|
||||
return util.TimeAfterOrBlock(-1)
|
||||
}
|
||||
return util.TimeAfterOrBlock(int(obj.timeout))
|
||||
}
|
||||
|
||||
// InvalidateID marks the id as no longer valid.
|
||||
func (obj *cuid) InvalidateID() {
|
||||
obj.id = 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// IsConverged is a helper function to the regular IsConverged method.
|
||||
func (obj *cuid) IsConverged() bool {
|
||||
return obj.converger.IsConverged(obj)
|
||||
}
|
||||
|
||||
// SetConverged is a helper function to the regular SetConverged notification.
|
||||
func (obj *cuid) SetConverged(isConverged bool) error {
|
||||
return obj.converger.SetConverged(obj, isConverged)
|
||||
}
|
||||
|
||||
// Unregister is a helper function to unregister myself.
|
||||
func (obj *cuid) Unregister() {
|
||||
obj.converger.Unregister(obj)
|
||||
}
|
||||
|
||||
// ConvergedTimer is a helper around the regular ConvergedTimer method.
|
||||
func (obj *cuid) ConvergedTimer() <-chan time.Time {
|
||||
return obj.converger.ConvergedTimer(obj)
|
||||
}
|
||||
|
||||
// StartTimer runs an invisible timer that automatically converges on timeout.
|
||||
func (obj *cuid) StartTimer() (func() error, error) {
|
||||
// StartTimer runs a timer that sets us as converged on timeout. It also returns
|
||||
// a handle to the StopTimer function which should be run before exit.
|
||||
func (obj *UID) StartTimer() (func() error, error) {
|
||||
obj.mutex.Lock()
|
||||
if !obj.running {
|
||||
obj.timer = make(chan struct{})
|
||||
obj.running = true
|
||||
} else {
|
||||
obj.mutex.Unlock()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
return obj.StopTimer, fmt.Errorf("timer already started")
|
||||
}
|
||||
obj.mutex.Unlock()
|
||||
obj.timer = make(chan struct{})
|
||||
obj.running = true
|
||||
obj.wg.Add(1)
|
||||
go func() {
|
||||
defer obj.wg.Done()
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
obj.SetConverged(false)
|
||||
|
||||
@@ -351,8 +448,8 @@ func (obj *cuid) StartTimer() (func() error, error) {
|
||||
obj.SetConverged(true) // converged!
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,8 +458,8 @@ func (obj *cuid) StartTimer() (func() error, error) {
|
||||
return obj.StopTimer, nil
|
||||
}
|
||||
|
||||
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
||||
func (obj *cuid) ResetTimer() error {
|
||||
// ResetTimer resets the timer to zero.
|
||||
func (obj *UID) ResetTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
@@ -372,8 +469,8 @@ func (obj *cuid) ResetTimer() error {
|
||||
return fmt.Errorf("timer hasn't been started")
|
||||
}
|
||||
|
||||
// StopTimer stops the running timer permanently until a StartTimer is run.
|
||||
func (obj *cuid) StopTimer() error {
|
||||
// StopTimer stops the running timer.
|
||||
func (obj *UID) StopTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if !obj.running {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,16 +15,17 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package etcd
|
||||
// +build !root
|
||||
|
||||
package converger
|
||||
|
||||
import (
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Client provides a simple interface specification for client requests. Both
|
||||
// EmbdEtcd and ClientEtcd implement this.
|
||||
type Client interface {
|
||||
// TODO: add more method signatures
|
||||
Get(path string, opts ...etcd.OpOption) (map[string]string, error)
|
||||
Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error)
|
||||
func TestBufferedChan1(t *testing.T) {
|
||||
ch := make(chan bool, 1)
|
||||
ch <- true
|
||||
close(ch) // closing a channel that's not empty should not block
|
||||
// must be able to exit without blocking anywhere
|
||||
}
|
||||
2
debian/copyright
vendored
2
debian/copyright
vendored
@@ -3,7 +3,7 @@ Upstream-Name: mgmt
|
||||
Source: <https://github.com/purpleidea/mgmt>
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
Copyright: Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
License: GPL-3.0
|
||||
|
||||
License: GPL-3.0
|
||||
|
||||
2
doc.go
2
doc.go
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM golang:1.9
|
||||
FROM golang:1.13
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2017-11-16
|
||||
ENV REFRESHED_AT 2020-09-23
|
||||
|
||||
# Update the package list to be able to use required packages
|
||||
RUN apt-get update
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM golang:1.9
|
||||
FROM golang:1.13
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2017-11-16
|
||||
ENV REFRESHED_AT 2019-02-06
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'mgmt'
|
||||
copyright = u'2013-2018+ James Shubin and the project contributors'
|
||||
copyright = u'2013-2021+ James Shubin and the project contributors'
|
||||
author = u'James Shubin'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
|
||||
@@ -3,7 +3,119 @@
|
||||
This document contains some additional information and help regarding
|
||||
developing `mgmt`. Useful tools, conventions, etc.
|
||||
|
||||
Be sure to read [quick start guide](docs/quick-start-guide.md) first.
|
||||
Be sure to read [quick start guide](quick-start-guide.md) first.
|
||||
|
||||
## Vagrant
|
||||
|
||||
If you would like to avoid doing the above steps manually, we have prepared a
|
||||
[Vagrant](https://www.vagrantup.com/) environment for your convenience. From the
|
||||
project directory, run a `vagrant up`, and then a `vagrant status`. From there,
|
||||
you can `vagrant ssh` into the `mgmt` machine. The `MOTD` will explain the rest.
|
||||
This environment isn't commonly used by the `mgmt` developers, so it might not
|
||||
be working properly.
|
||||
|
||||
## Using Docker
|
||||
|
||||
Alternatively, you can check out the [docker-guide](docker-guide.md) in order to
|
||||
develop or deploy using docker. This method is not endorsed or supported, so use
|
||||
at your own risk, as it might not be working properly.
|
||||
|
||||
## Information about dependencies
|
||||
|
||||
Software projects have a few different kinds of dependencies. There are _build_
|
||||
dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
|
||||
required for running the _test_ suite.
|
||||
|
||||
### Build
|
||||
|
||||
* `golang` 1.13 or higher (required, available in some distros and distributed
|
||||
as a binary officially by [golang.org](https://golang.org/dl/))
|
||||
|
||||
### Runtime
|
||||
|
||||
A relatively modern GNU/Linux system should be able to run `mgmt` without any
|
||||
problems. Since `mgmt` runs as a single statically compiled binary, all of the
|
||||
library dependencies are included. It is expected, that certain advanced
|
||||
resources require host specific facilities to work. These requirements are
|
||||
listed below:
|
||||
|
||||
| Resource | Dependency | Version | Check version with |
|
||||
|----------|-------------------|-----------------------------|-----------------------------------------------------------|
|
||||
| augeas | augeas-devel | `augeas 1.6` or greater | `dnf info augeas-devel` or `apt-cache show libaugeas-dev` |
|
||||
| file | inotify | `Linux 2.6.27` or greater | `uname -a` |
|
||||
| hostname | systemd-hostnamed | `systemd 25` or greater | `systemctl --version` |
|
||||
| nspawn | systemd-nspawn | `systemd ???` or greater | `systemctl --version` |
|
||||
| pkg | packagekitd | `packagekit 1.x` or greater | `pkcon --version` |
|
||||
| svc | systemd | `systemd ???` or greater | `systemctl --version` |
|
||||
| virt | libvirt-devel | `libvirt 1.2.0` or greater | `dnf info libvirt-devel` or `apt-cache show libvirt-dev` |
|
||||
| virt | libvirtd | `libvirt 1.2.0` or greater | `libvirtd --version` |
|
||||
|
||||
For building a visual representation of the graph, `graphviz` is required.
|
||||
|
||||
To build `mgmt` without augeas support please run:
|
||||
`GOTAGS='noaugeas' make build`
|
||||
|
||||
To build `mgmt` without libvirt support please run:
|
||||
`GOTAGS='novirt' make build`
|
||||
|
||||
To build `mgmt` without docker support please run:
|
||||
`GOTAGS='nodocker' make build`
|
||||
|
||||
To build `mgmt` without augeas, libvirt or docker support please run:
|
||||
`GOTAGS='noaugeas novirt nodocker' make build`
|
||||
|
||||
## OSX/macOS/Darwin development
|
||||
|
||||
Developing and running `mgmt` on macOS is currently not supported (but not
|
||||
discouraged either). Meaning it might work but in the case it doesn't you would
|
||||
have to provide your own patches to fix problems (the project maintainer and
|
||||
community are glad to assist where needed).
|
||||
|
||||
There are currently some issues that make `mgmt` less suitable to run for
|
||||
provisioning macOS. But as a client to provision remote servers it should run
|
||||
fine.
|
||||
|
||||
Since the primary supported systems are Linux and these are the environments
|
||||
tested, it is wise to run these suites during macOS development as well. To ease
|
||||
this, Docker can be leveraged ([Docker for Mac](https://docs.docker.com/docker-for-mac/)).
|
||||
|
||||
Before running any of the commands below create the development Docker image:
|
||||
|
||||
```
|
||||
docker/scripts/build-development
|
||||
```
|
||||
|
||||
This image requires updating every time dependencies (`make-deps.sh`) changes.
|
||||
|
||||
Then to run the test suite:
|
||||
|
||||
```
|
||||
docker run --rm -ti \
|
||||
-v $PWD:/go/src/github.com/purpleidea/mgmt/ \
|
||||
-w /go/src/github.com/purpleidea/mgmt/ \
|
||||
purpleidea/mgmt:development \
|
||||
make test
|
||||
```
|
||||
|
||||
For convenience this command is wrapped in `docker/scripts/exec-development`.
|
||||
|
||||
Basically any command can be executed this way. Because the repository source is
|
||||
mounted into the Docker container invocation will be quick and allow rapid
|
||||
testing, for example:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development test/test-shell.sh load0.sh
|
||||
```
|
||||
|
||||
Other examples:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development make build
|
||||
docker/scripts/exec-development ./mgmt run --tmp-prefix lang examples/lang/load0.mcl
|
||||
```
|
||||
|
||||
Be advised that this method is not supported and it might not be working
|
||||
properly.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -45,5 +157,6 @@ individual tests to run.
|
||||
|
||||
### IDE/Editor support
|
||||
|
||||
- Emacs: see `misc/emacs/`
|
||||
- [Textmate](https://github.com/aequitas/mgmt.tmbundle)
|
||||
* Emacs: see `misc/emacs/`
|
||||
* [Textmate](https://github.com/aequitas/mgmt.tmbundle)
|
||||
* [VSCode](https://github.com/aequitas/mgmt.vscode)
|
||||
|
||||
@@ -122,6 +122,10 @@ entire set of running mgmt agents will need to all simultaneously converge for
|
||||
the group to exit. This is particularly useful for bootstrapping new clusters
|
||||
which need to exchange information that is only available at run time.
|
||||
|
||||
This existed in earlier versions of mgmt as a `--remote` option, but it has been
|
||||
removed and is being ported to a more powerful variant where you can remote
|
||||
execute via a `remote` resource.
|
||||
|
||||
#### Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
@@ -137,17 +141,17 @@ Invoke `mgmt` with the `--puppet` switch, which supports 3 variants:
|
||||
|
||||
1. Request the configuration from the Puppet Master (like `puppet agent` does)
|
||||
|
||||
`mgmt run --puppet agent`
|
||||
`mgmt run puppet --puppet agent`
|
||||
|
||||
2. Compile a local manifest file (like `puppet apply`)
|
||||
|
||||
`mgmt run --puppet /path/to/my/manifest.pp`
|
||||
`mgmt run puppet --puppet /path/to/my/manifest.pp`
|
||||
|
||||
3. Compile an ad hoc manifest from the commandline (like `puppet apply -e`)
|
||||
|
||||
`mgmt run --puppet 'file { "/etc/ntp.conf": ensure => file }'`
|
||||
`mgmt run puppet --puppet 'file { "/etc/ntp.conf": ensure => file }'`
|
||||
|
||||
For more details and caveats see [Puppet.md](Puppet.md).
|
||||
For more details and caveats see [puppet-guide.md](puppet-guide.md).
|
||||
|
||||
#### Blog post
|
||||
|
||||
@@ -164,6 +168,7 @@ If you feel that a well used option needs documenting here, please patch it!
|
||||
### Overview of reference
|
||||
|
||||
* [Meta parameters](#meta-parameters): List of available resource meta parameters.
|
||||
* [Lang metadata file](#lang-metadata-file): Lang metadata file format.
|
||||
* [Graph definition file](#graph-definition-file): Main graph definition file.
|
||||
* [Command line](#command-line): Command line parameters.
|
||||
* [Compilation options](#compilation-options): Compilation options.
|
||||
@@ -249,21 +254,91 @@ integer, then that value is the max size for that semaphore. Valid semaphore
|
||||
id's include: `some_id`, `hello:42`, `not:smart:4` and `:13`. It is expected
|
||||
that the last bare example be only used by the engine to add a global semaphore.
|
||||
|
||||
#### Rewatch
|
||||
|
||||
Boolean. Rewatch specifies whether we re-run the Watch worker during a graph
|
||||
swap if it has errored. When doing a graph compare to swap the graphs, if this
|
||||
is true, and this particular worker has errored, then we'll remove it and add it
|
||||
back as a new vertex, thus causing it to run again. This is different from the
|
||||
`Retry` metaparam which applies during the normal execution. It is only when
|
||||
this is exhausted that we're in permanent worker failure, and only then can we
|
||||
rely on this metaparam.
|
||||
|
||||
#### Realize
|
||||
|
||||
Boolean. Realize ensures that the resource is guaranteed to converge at least
|
||||
once before a potential graph swap removes or changes it. This guarantee is
|
||||
useful for fast changing graphs, to ensure that the brief creation of a resource
|
||||
is seen. This guarantee does not prevent against the engine quitting normally,
|
||||
and it can't guarantee it if the resource is blocked because of a failed
|
||||
pre-requisite resource.
|
||||
*XXX: This is currently not implemented!*
|
||||
|
||||
#### Reverse
|
||||
|
||||
Boolean. Reverse is a property that some resources can implement that specifies
|
||||
that some "reverse" operation should happen when that resource "disappears". A
|
||||
disappearance happens when a resource is defined in one instance of the graph,
|
||||
and is gone in the subsequent one. This disappearance can happen if it was
|
||||
previously in an if statement that then becomes false.
|
||||
|
||||
This is helpful for building robust programs with the engine. The engine adds a
|
||||
"reversed" resource to that subsequent graph to accomplish the desired "reverse"
|
||||
mechanics. The specifics of what this entails is a property of the particular
|
||||
resource that is being "reversed".
|
||||
|
||||
It might be wise to combine the use of this meta parameter with the use of the
|
||||
`realize` meta parameter to ensure that your reversed resource actually runs at
|
||||
least once, if there's a chance that it might be gone for a while.
|
||||
|
||||
### Lang metadata file
|
||||
|
||||
Any module *must* have a metadata file in its root. It must be named
|
||||
`metadata.yaml`, even if it's empty. You can specify zero or more values in yaml
|
||||
format which can change how your module behaves, and where the `mcl` language
|
||||
looks for code and other files. The most important top level keys are: `main`,
|
||||
`path`, `files`, and `license`.
|
||||
|
||||
#### Main
|
||||
|
||||
The `main` key points to the default entry point of your code. It must be a
|
||||
relative path if specified. If it's empty it defaults to `main.mcl`. It should
|
||||
generally not be changed. It is sometimes set to `main/main.mcl` if you'd like
|
||||
your modules code out of the root and into a child directory for cases where you
|
||||
don't plan on having a lot deeper imports relative to `main.mcl` and all those
|
||||
files would clutter things up.
|
||||
|
||||
#### Path
|
||||
|
||||
The `path` key specifies the modules import search directory to use for this
|
||||
module. You can specify this if you'd like to vendor something for your module.
|
||||
In general, if you use it, please use the convention: `path/`. If it's not
|
||||
specified, you will default to the parent modules directory.
|
||||
|
||||
#### Files
|
||||
|
||||
The `files` key specifies some additional files that will get included in your
|
||||
deploy. It defaults to `files/`.
|
||||
|
||||
#### License
|
||||
|
||||
The `license` key allows you to specify a license for the module. Please specify
|
||||
one so that everyone can enjoy your code! Use a "short license identifier", like
|
||||
`LGPLv3+`, or `MIT`. The former is a safe choice if you're not sure what to use.
|
||||
|
||||
### Graph definition file
|
||||
|
||||
graph.yaml is the compiled graph definition file. The format is currently
|
||||
undocumented, but by looking through the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
you can probably figure out most of it, as it's fairly intuitive.
|
||||
undocumented, but by looking through the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples/yaml/)
|
||||
you can probably figure out most of it, as it's fairly intuitive. It's not
|
||||
recommended that you use this, since it's preferable to write code in the
|
||||
[mcl language](language-guide.md) front-end.
|
||||
|
||||
### Command line
|
||||
|
||||
The main interface to the `mgmt` tool is the command line. For the most recent
|
||||
documentation, please run `mgmt --help`.
|
||||
|
||||
#### `--yaml <graph.yaml>`
|
||||
|
||||
Point to a graph file to run.
|
||||
|
||||
#### `--converged-timeout <seconds>`
|
||||
|
||||
Exit if the machine has converged for approximately this many seconds.
|
||||
@@ -289,12 +364,6 @@ collision with this globally defined semaphore. The size value must be greater
|
||||
than zero at this time. The traditional non-parallel execution found in config
|
||||
management tools such as `Puppet` can be obtained with `--sema 1`.
|
||||
|
||||
#### `--remote <graph.yaml>`
|
||||
|
||||
Point to a graph file to run on the remote host specified within. This parameter
|
||||
can be used multiple times if you'd like to remotely run on multiple hosts in
|
||||
parallel.
|
||||
|
||||
#### `--allow-interactive`
|
||||
|
||||
Allow interactive prompting for SSH passwords if there is no authentication
|
||||
@@ -333,8 +402,8 @@ default prefix. This can't be combined with the `--prefix` option.
|
||||
If this option is specified, we will attempt to fall back to a temporary prefix
|
||||
if the primary prefix couldn't be created. This is useful for avoiding failures
|
||||
in environments where the primary prefix may or may not be available, but you'd
|
||||
like to try. The canonical example is when running `mgmt` with `--remote` there
|
||||
might be a cached copy of the binary in the primary prefix, but in case there's
|
||||
like to try. The canonical example is when running `mgmt` with remote execution
|
||||
there might be a cached copy of the binary in the primary prefix, but if there's
|
||||
no binary available continue working in a temporary directory to avoid failure.
|
||||
|
||||
### Compilation options
|
||||
@@ -351,18 +420,28 @@ GOTAGS=novirt make build
|
||||
|
||||
#### Disable augeas support
|
||||
|
||||
If you wish to compile mgmt without augeas support, you can use the following command:
|
||||
If you wish to compile mgmt without augeas support, you can use the following
|
||||
command:
|
||||
|
||||
```
|
||||
GOTAGS=noaugeas make build
|
||||
```
|
||||
|
||||
#### Disable docker support
|
||||
|
||||
If you wish to compile mgmt without docker support, you can use the following
|
||||
command:
|
||||
|
||||
```
|
||||
GOTAGS=nodocker make build
|
||||
```
|
||||
|
||||
#### Combining compile-time flags
|
||||
|
||||
You can combine multiple tags by using a space-separated list:
|
||||
|
||||
```
|
||||
GOTAGS="noaugeas novirt" make build
|
||||
GOTAGS="noaugeas novirt nodocker" make build
|
||||
```
|
||||
|
||||
## Examples
|
||||
@@ -407,7 +486,7 @@ To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt
|
||||
|
||||
## Authors
|
||||
|
||||
Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
|
||||
Please see the
|
||||
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
||||
|
||||
192
docs/faq.md
192
docs/faq.md
@@ -9,6 +9,18 @@ I wanted a next generation config management solution that didn't have all of
|
||||
the design flaws or limitations that the current generation of tools do, and no
|
||||
tool existed!
|
||||
|
||||
### Why did you choose `golang` for the project?
|
||||
|
||||
When I started working on the project, I needed to choose a language that
|
||||
already had an implementation of a distributed consensus algorithm available.
|
||||
That meant [Paxos](https://en.wikipedia.org/wiki/Paxos_(computer_science)) or
|
||||
[Raft](https://en.wikipedia.org/wiki/Raft_(computer_science)). Golang was one
|
||||
language that actually had two different Raft implementations, `etcd`, and
|
||||
`consul`. Other design requirements included something that was reasonably fast,
|
||||
typed and memory-safe, and suited for systems engineering. After a reasonably
|
||||
extensive search, I chose `golang`. I think it was the right decision. There are
|
||||
a number of other features of the language which helped influence the decision.
|
||||
|
||||
### How do I contribute to the project if I don't know `golang`?
|
||||
|
||||
There are many different ways you can contribute to the project. They can be
|
||||
@@ -41,10 +53,11 @@ find a number of tutorials online.
|
||||
3. Spend between four to six hours with the [golang tour](https://tour.golang.org/).
|
||||
Skip over the longer problems, but try and get a solid overview of everything.
|
||||
If you forget something, you can always go back and repeat those parts.
|
||||
4. Connect to our [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
IRC channel on the [Freenode](https://freenode.net/) network. You can use any
|
||||
IRC client that you'd like, but the [hosted web portal](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
will suffice if you don't know what else to use.
|
||||
4. Connect to our [#mgmtconfig](https://web.libera.chat/?channels=#mgmtconfig)
|
||||
IRC channel on the [Libera.Chat](https://libera.chat/) network. You can use any
|
||||
IRC client that you'd like, but the [hosted web portal](https://web.libera.chat/?channels=#mgmtconfig)
|
||||
will suffice if you don't know what else to use. [Here are a few suggestions for
|
||||
alternative clients.](https://libera.chat/guides/clients)
|
||||
5. Now it's time to try and starting writing a patch! We have tagged a bunch of
|
||||
[open issues as #mgmtlove](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
|
||||
for new users to have somewhere to get involved. Look through them to see if
|
||||
@@ -57,6 +70,8 @@ hacking!
|
||||
|
||||
### Is this project ready for production?
|
||||
|
||||
It's getting pretty close. I'm able to write modules for it now!
|
||||
|
||||
Compared to some existing automation tools out there, mgmt is a relatively new
|
||||
project. It is probably not as feature complete as some other software, but it
|
||||
also offers a number of features which are not currently available elsewhere.
|
||||
@@ -123,6 +138,58 @@ The downside to this approach is that you won't benefit from the automatic
|
||||
elastic nature of the embedded etcd servers, and that you're responsible if you
|
||||
accidentally break your etcd cluster, or if you use an unsupported version.
|
||||
|
||||
### In `mgmt` you talk about events. What is this referring to?
|
||||
|
||||
Mgmt has two main concepts that involve "events":
|
||||
1. Events in the [resource primitive](resource-guide.md).
|
||||
2. Events in the [reactive language](language-guide.md).
|
||||
|
||||
Each resource primitive in mgmt can test (check) and set (apply) the desired
|
||||
state that was requested of it. This is familiar to what is common with existing
|
||||
tools such as `Puppet`, `Ansible`, `Chef`, `Terraform`, etc... In addition,
|
||||
`mgmt` can also **watch** the state and detect changes. As a result, it never
|
||||
has to waste time and cpu resources by polling to test and set state, leading to
|
||||
a design which is algorithmically much faster than the existing generation of
|
||||
tools.
|
||||
|
||||
To describe the set of resources to apply, mgmt describes this collection with a
|
||||
language. In order to model the time component of infrastructure, we use a
|
||||
special kind of language called an [FRP](https://en.wikipedia.org/wiki/Functional_reactive_programming).
|
||||
This language has a built-in concept that we call "events", and which means that
|
||||
we re-evaluate the relevant portions of the code whenever a value or function
|
||||
has an event that tells us that it changed. The `R` in `FRP` stands for
|
||||
reactive. This is similar to how a spreadsheet updates dependent cells when a
|
||||
pre-requisite value is modified. [This article](https://en.wikipedia.org/wiki/Reactive_programming)
|
||||
provides a bit more background.
|
||||
|
||||
Whenever any of the streams of values in the language change, the program is
|
||||
partially re-evaluated. The output of any mgmt program is a [DAG](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
|
||||
of resources, or more precisely, a stream of resource graphs. Since we have
|
||||
events per-resource, we can efficiently switch from one desired-state resource
|
||||
graph to the next without re-checking their individual states, since we've been
|
||||
monitoring them all along.
|
||||
|
||||
One side-effect of all this, is that if a rogue systems administrator manually
|
||||
changes the state of any managed resource, mgmt will detect this and attempt to
|
||||
revert the change. This makes for excellent live demos, but is not the primary
|
||||
design goal. It is a consequence of tracking state so that graph changes are
|
||||
efficient. We implement the event detection via an intentional per-resource
|
||||
[main loop](https://en.wikipedia.org/wiki/Event_loop) which can enable other
|
||||
interesting functionality too!
|
||||
|
||||
Make sure to get rid of your rogue sysadmin! ;)
|
||||
|
||||
### Do I need to run `mgmt` as `root`?
|
||||
|
||||
No and yes. It depends. Nothing in mgmt explicitly requires root in the design,
|
||||
however mgmt will require root only if the changes to your system that you want
|
||||
it to make require root.
|
||||
|
||||
For example, if you use it to manage files that require root access to modify,
|
||||
then you'll need root. If you only use it to manage files and resources
|
||||
elsewhere, then it shouldn't need root. Many resources are perfectly usable
|
||||
without root, and virtually all of my live demos are done without root.
|
||||
|
||||
### How can I run `mgmt` on-demand, or in `cron`, instead of continuously?
|
||||
|
||||
By default, `mgmt` will run continuously in an attempt to keep your machine in a
|
||||
@@ -146,42 +213,94 @@ requires a number of seconds as an argument.
|
||||
#### Example:
|
||||
|
||||
```
|
||||
./mgmt run --lang examples/lang/hello0.mcl --converged-timeout=5
|
||||
./mgmt run lang examples/lang/hello0.mcl --converged-timeout=5
|
||||
```
|
||||
|
||||
### What does the error message about an inconsistent dataDir mean?
|
||||
### When I try to build `mgmt` I see: `no Go files in $GOPATH/src/github.com/purpleidea/mgmt/bindata`.
|
||||
|
||||
Due to the arcane way that `golang` designed its `$GOPATH`, the main project
|
||||
directory must be inside your `$GOPATH`, and at the appropriate FQDN. This is:
|
||||
`$GOPATH/src/github.com/purpleidea/mgmt/`. If you have your project root outside
|
||||
of that directory, then you may get this error when you try to build it. In this
|
||||
case there is likely a `go get` version of the project at this location. Remove
|
||||
it and replace it with your git cloned directory. In my case, I like to work on
|
||||
things in `~/code/mgmt/`, so that path is a symlink that points to the long
|
||||
project directory.
|
||||
|
||||
### Why does my file resource error with `no such file or directory`?
|
||||
|
||||
If you create a file resource and only specify the content like this:
|
||||
|
||||
```
|
||||
file "/tmp/foo" {
|
||||
content => "hello world\n",
|
||||
}
|
||||
```
|
||||
|
||||
Then this will attempt to set the contents of that file to the desired string,
|
||||
but *only* if that file already exists. If you'd like to ensure that it also
|
||||
gets created in case it is not present, then you must also specify the state:
|
||||
|
||||
```
|
||||
file "/tmp/foo" {
|
||||
state => $const.res.file.state.exists,
|
||||
content => "hello world\n",
|
||||
}
|
||||
```
|
||||
|
||||
Similar logic applies for situations when you only specify the `mode` parameter.
|
||||
|
||||
This all turns out to be more safe and "correct", in that it would error and
|
||||
prevent masking an error for a situation when you expected a file to already be
|
||||
at that location. It also turns out to simplify the internals significantly, and
|
||||
remove an ambiguous scenario with the reversable file resource.
|
||||
|
||||
### Why do function names inside of templates include underscores?
|
||||
|
||||
The golang template library which we use to implement the template() function
|
||||
doesn't support the dot notation, so we import all our normal functions, and
|
||||
just replace dots with underscores. As an example, the standard `datetime.print`
|
||||
function is shown within mcl scripts as datetime_print after being imported.
|
||||
|
||||
### On startup `mgmt` hangs after: `etcd: server: starting...`.
|
||||
|
||||
If you get an error message similar to:
|
||||
|
||||
```
|
||||
Etcd: Connect: CtxError...
|
||||
Etcd: CtxError: Reason: CtxDelayErr(5s): No endpoints available yet!
|
||||
Etcd: Connect: Endpoints: []
|
||||
Etcd: The dataDir (/var/lib/mgmt/etcd) might be inconsistent or corrupt.
|
||||
etcd: server: starting...
|
||||
etcd: server: start timeout of 1m0s reached
|
||||
etcd: server: close timeout of 15s reached
|
||||
```
|
||||
|
||||
This happens when there are a series of fatal connect errors in a row. This can
|
||||
happen when you start `mgmt` using a dataDir that doesn't correspond to the
|
||||
current cluster view. As a result, the embedded etcd server never finishes
|
||||
starting up, and as a result, a default endpoint never gets added. The solution
|
||||
is to either reconcile the mistake, and if there is no important data saved, you
|
||||
can remove the etcd dataDir. This is typically `/var/lib/mgmt/etcd/member/`.
|
||||
But nothing happens afterwards, this can be due to a corrupt etcd storage
|
||||
directory. Each etcd server embedded in mgmt must have a special directory where
|
||||
it stores local state. It must not be shared by more than one individual member.
|
||||
This dir is typically `/var/lib/mgmt/etcd/member/`. If you accidentally use it
|
||||
(for example during testing) with a different cluster view, then you can corrupt
|
||||
it. This can happen if you use it with more than one different hostname.
|
||||
|
||||
### Why do resources have both a `Compare` method and an `IFF` (on the UID) method?
|
||||
The solution is to avoid making this mistake, and if there is no important data
|
||||
saved, you can remove the etcd member dir and start over.
|
||||
|
||||
The `Compare()` methods are for determining if two resources are effectively the
|
||||
same, which is used to make graph change delta's efficient. This is when we want
|
||||
to change from the current running graph to a new graph, but preserve the common
|
||||
vertices. Since we want to make this process efficient, we only update the parts
|
||||
that are different, and leave everything else alone. This `Compare()` method can
|
||||
tell us if two resources are the same.
|
||||
### On running `make` to build a new version, it errors with: `Text file busy`.
|
||||
|
||||
The `IFF()` method is part of the whole UID system, which is for discerning if a
|
||||
resource meets the requirements another expects for an automatic edge. This is
|
||||
because the automatic edge system assumes a unified UID pattern to test for
|
||||
equality. In the future it might be helpful or sane to merge the two similar
|
||||
comparison functions although for now they are separate because they are
|
||||
actually answer different questions.
|
||||
If you get an error like:
|
||||
|
||||
```
|
||||
cp: cannot create regular file 'mgmt': Text file busy
|
||||
```
|
||||
|
||||
This can happen if you ran `make build` (or just `make`) when there was already
|
||||
an instance of mgmt running, or if a related file locking issue occurred. To
|
||||
solve this, shutdown and running mgmt process, run `rm mgmt` to remove the file,
|
||||
and then get a new one by running `make` again.
|
||||
|
||||
### The docs speaks of `--remote` but the CLI errors out?
|
||||
|
||||
The `--remote` flag existed in an earlier version of mgmt. It was removed and
|
||||
will be replaced with a more powerful version, which is a "remote" resource. The
|
||||
code is mostly ready but it's not finished. If you'd like to help finish it or
|
||||
sponsor the work, please let me know.
|
||||
|
||||
### Does this support Windows? OSX? GNU Hurd?
|
||||
|
||||
@@ -190,7 +309,7 @@ serious automation workloads. Support for non-Linux operating systems isn't a
|
||||
high priority of mine, but we're happy to accept patches for missing features
|
||||
or resources that you think would make sense on your favourite platform.
|
||||
|
||||
### Why aren't you using `glide` or `godep` for dependency management?
|
||||
### Why aren't you using `glide`, `godep` or `go mod` for dependency management?
|
||||
|
||||
Vendoring dependencies means that as the git master branch of each dependency
|
||||
marches on, you're left behind using an old version. As a result, bug fixes and
|
||||
@@ -258,10 +377,9 @@ which definitely existed before the band did.
|
||||
|
||||
### You didn't answer my question, or I have a question!
|
||||
|
||||
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
to see if someone can help you. Once we get a big enough community going, we'll
|
||||
add a mailing list. If you don't get any response from the above, you can
|
||||
contact me through my [technical blog](https://purpleidea.com/contact/)
|
||||
and I'll do my best to help. If you have a good question, please add it as a
|
||||
patch to this documentation. I'll merge your question, and add a patch with the
|
||||
answer!
|
||||
It's best to ask on [IRC](https://web.libera.chat/?channels=#mgmtconfig)
|
||||
to see if someone can help you. If you don't get a response from IRC, you can
|
||||
contact me through my [technical blog](https://purpleidea.com/contact/) and I'll
|
||||
do my best to help. If you have a good question, please add it as a patch to
|
||||
this documentation. I'll merge your question, and add a patch with the answer!
|
||||
For news and updates, subscribe to the [mailing list](https://www.redhat.com/mailman/listinfo/mgmtconfig-list).
|
||||
|
||||
@@ -37,8 +37,10 @@ available types and values in the mgmt language. It is very easy to use, and
|
||||
should be fairly intuitive. Most of what you'll need to know can be inferred
|
||||
from looking at example code.
|
||||
|
||||
To implement a function, you'll need to create a file in
|
||||
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/).
|
||||
To implement a function, you'll need to create a file that imports the
|
||||
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/)
|
||||
module. It should probably get created in the correct directory inside of:
|
||||
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||
The function should be implemented as a `FuncValue` in our type system. It is
|
||||
then registered with the engine during `init()`. An example explains it best:
|
||||
|
||||
@@ -50,14 +52,15 @@ package simple
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/lang/funcs/simple"
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
)
|
||||
|
||||
// you must register your functions in init when the program starts up
|
||||
func init() {
|
||||
// Example function that squares an int and prints out answer as an str.
|
||||
Register("talkingsquare", &types.FuncValue{
|
||||
T: types.NewType("func(a int) str"), // declare the signature
|
||||
simple.ModuleRegister(ModuleName, "talkingsquare", &types.FuncValue{
|
||||
T: types.NewType("func(int) str"), // declare the signature
|
||||
V: func(input []types.Value) (types.Value, error) {
|
||||
i := input[0].Int() // get first arg as an int64
|
||||
// must return the above specified value
|
||||
@@ -109,31 +112,37 @@ As with the simple, non-polymorphic API, you can only implement [pure](https://e
|
||||
functions, without writing too much boilerplate code. They will be automatically
|
||||
re-evaluated as needed when their input values change.
|
||||
|
||||
To implement a function, you'll need to create a file in
|
||||
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/).
|
||||
To implement a function, you'll need to create a file that imports the
|
||||
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/)
|
||||
module. It should probably get created in the correct directory inside of:
|
||||
[`lang/funcs/core/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||
The function should be implemented as a list of `FuncValue`'s in our type
|
||||
system. It is then registered with the engine during `init()`. You may also use
|
||||
the `variant` type in your type definitions. This special type will never be
|
||||
seen inside a running program, and will get converted to a concrete type if a
|
||||
suitable match to this signature can be found. Be warned that signatures which
|
||||
contain too many variants, or which are very general, might be hard for the
|
||||
compiler to match, and ambiguous type graphs make for user compiler errors.
|
||||
compiler to match, and ambiguous type graphs make for user compiler errors. The
|
||||
top-level type must still be a function type, it may only contain variants as
|
||||
part of its signature. It is probably more difficult to unify a function if its
|
||||
return type is a variant, as opposed to if one of its args was.
|
||||
|
||||
An example explains it best:
|
||||
|
||||
### Example
|
||||
|
||||
```golang
|
||||
package simplepoly
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("len", []*types.FuncValue{
|
||||
// You may use the simplepoly.ModuleRegister method to register your
|
||||
// function if it's in a module, as seen in the simple function example.
|
||||
simplepoly.Register("len", []*types.FuncValue{
|
||||
{
|
||||
T: types.NewType("func([]variant) int"),
|
||||
V: Len,
|
||||
@@ -191,7 +200,7 @@ if it meets your needs. Most functions will be able to use that API. If you
|
||||
really need something more powerful, then you can use the regular function API.
|
||||
What follows are each of the method signatures and a description of each.
|
||||
|
||||
### Default
|
||||
### Info
|
||||
|
||||
```golang
|
||||
Info() *interfaces.Info
|
||||
@@ -220,7 +229,7 @@ Init(init *interfaces.Init) error
|
||||
|
||||
This is called to initialize the function. If something goes wrong, it should
|
||||
return an error. It is passed a struct that contains all the important
|
||||
information and poiinters that it might need to work with throughout its
|
||||
information and pointers that it might need to work with throughout its
|
||||
lifetime. As a result, it will need to save a copy to that pointer for future
|
||||
use in the other methods.
|
||||
|
||||
@@ -343,11 +352,21 @@ also ensures they can be encoded and decoded. Make sure to include the following
|
||||
code snippet for this to work.
|
||||
|
||||
```golang
|
||||
import "github.com/purpleidea/mgmt/lang/funcs"
|
||||
|
||||
func init() { // special golang method that runs once
|
||||
funcs.Register("foo", func() interfaces.Func { return &FooFunc{} })
|
||||
}
|
||||
```
|
||||
|
||||
Functions inside of built-in modules will need to use the `ModuleRegister`
|
||||
method instead.
|
||||
|
||||
```golang
|
||||
// moduleName is already set to "math" by the math package. Do this in `init`.
|
||||
funcs.ModuleRegister(moduleName, "cos", func() interfaces.Func { return &CosFunc{} })
|
||||
```
|
||||
|
||||
### Composite functions
|
||||
|
||||
Composite functions are functions which import one or more existing functions.
|
||||
@@ -368,9 +387,9 @@ might be different ways you would want to call `printf`, such as:
|
||||
`printf("the %s is %d", "answer", 42)` or `printf("3 * 2 = %d", 3 * 2)`. Since
|
||||
you couldn't implement the infinite number of possible signatures, this API lets
|
||||
you write code which can be coerced into different forms. This makes
|
||||
implementing what would appear to be generic or polymorphic, instead something
|
||||
that is actually static and that still has the static type safety properties
|
||||
that were guaranteed by the mgmt language.
|
||||
implementing what would appear to be generic or polymorphic, instead of
|
||||
something that is actually static and that still has the static type safety
|
||||
properties that were guaranteed by the mgmt language.
|
||||
|
||||
Since this is an advanced topic, it is not described in full at this time. For
|
||||
more information please have a look at the source code comments, some of the
|
||||
@@ -426,6 +445,11 @@ generator to build your `FuncValue` implementations, and pass in the unique
|
||||
signature to each one as you are building them. Using a generator is a common
|
||||
technique which was mentioned previously.
|
||||
|
||||
One obvious situation where this might occur is if your function doesn't take
|
||||
any inputs! An example `math.fortytwo()` function was implemented that
|
||||
demonstrates the use of function generators to pass the type signatures into the
|
||||
implementations.
|
||||
|
||||
### Where can I find more information about mgmt?
|
||||
|
||||
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
||||
|
||||
@@ -54,7 +54,7 @@ can be impossible to infer the item's type.
|
||||
|
||||
An unordered set of unique keys of the same type and corresponding value pairs
|
||||
of another type, eg:
|
||||
`{"boiling" => 100, "freezing" => 0, "room" => "25", "house" => 22, "canada" => -30,}`.
|
||||
`{"boiling" => 100, "freezing" => 0, "room" => 25, "house" => 22, "canada" => -30,}`.
|
||||
That is to say, all of the keys must have the same type, and all of the values
|
||||
must have the same type. You can use any type for either, although it is
|
||||
probably advisable to avoid using very complex types as map keys.
|
||||
@@ -85,8 +85,9 @@ These docs will be expanded on when things are more certain to be stable.
|
||||
|
||||
There are a very small number of statements in our language. They include:
|
||||
|
||||
- **bind**: bind's an expression to a variable within that scope
|
||||
- **bind**: bind's an expression to a variable within that scope without output
|
||||
- eg: `$x = 42`
|
||||
|
||||
- **if**: produces up to one branch of statements based on a conditional
|
||||
expression
|
||||
|
||||
@@ -114,6 +115,56 @@ expression
|
||||
File["/tmp/hello"] -> Print["alert4"]
|
||||
```
|
||||
|
||||
- **class**: bind's a list of statements to a class name in scope without output
|
||||
|
||||
```mcl
|
||||
class foo {
|
||||
# some statements go here
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```mcl
|
||||
class bar($a, $b) { # a parameterized class
|
||||
# some statements go here
|
||||
}
|
||||
```
|
||||
|
||||
- **include**: include a particular class at this location producing output
|
||||
|
||||
```mcl
|
||||
include foo
|
||||
|
||||
include bar("hello", 42)
|
||||
include bar("world", 13) # an include can be called multiple times
|
||||
```
|
||||
|
||||
- **import**: import a particular scope from this location at a given namespace
|
||||
|
||||
```mcl
|
||||
# a system module import
|
||||
import "fmt"
|
||||
|
||||
# a local, single file import (relative path, not a module)
|
||||
import "dir1/file.mcl"
|
||||
|
||||
# a local, module import (relative path, contents are a module)
|
||||
import "dir2/"
|
||||
|
||||
# a remote module import (absolute remote path, contents are a module)
|
||||
import "git://github.com/purpleidea/mgmt-example1/"
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```mcl
|
||||
import "fmt" as * # contents namespaced into top-level names
|
||||
import "foo.mcl" # namespaced as foo
|
||||
import "dir1/" as bar # namespaced as bar
|
||||
import "git://github.com/purpleidea/mgmt-example1/" # namespaced as example1
|
||||
```
|
||||
|
||||
All statements produce _output_. Output consists of between zero and more
|
||||
`edges` and `resources`. A resource statement can produce a resource, whereas an
|
||||
`if` statement produces whatever the chosen branch produces. Ultimately the goal
|
||||
@@ -139,6 +190,8 @@ resource to control how it behaves. For example, setting the `content` parameter
|
||||
of a `file` resource to the string `hello`, will cause the contents of that file
|
||||
to contain the string `hello` after it has run.
|
||||
|
||||
##### Undefined parameters
|
||||
|
||||
For some parameters, there is a distinction between an unspecified parameter,
|
||||
and a parameter with a `zero` value. For example, for the file resource, you
|
||||
might choose to set the `content` parameter to be the empty string, which would
|
||||
@@ -153,7 +206,7 @@ value to use if that boolean is true. You can do this with the resource-specific
|
||||
$b = true # change me to false and then try editing the file manually
|
||||
file "/tmp/mgmt-elvis" {
|
||||
content => $b ?: "hello world\n",
|
||||
state => "exists",
|
||||
state => $const.res.file.state.exists,
|
||||
}
|
||||
```
|
||||
|
||||
@@ -163,6 +216,75 @@ it evaluates to `true`, then the parameter will be used. If no `elvis` operator
|
||||
is specified, then the parameter value will also be used. If the parameter is
|
||||
not specified, then it will obviously not be used.
|
||||
|
||||
##### Meta parameters
|
||||
|
||||
Resources may specify meta parameters. To do so, you must add them as you would
|
||||
a regular parameter, except that they start with `Meta` and are capitalized. Eg:
|
||||
|
||||
```mcl
|
||||
file "/tmp/f1" {
|
||||
content => "hello!\n",
|
||||
|
||||
Meta:noop => true,
|
||||
Meta:delay => $b ?: 42,
|
||||
Meta:autoedge => false,
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, they also support the elvis operator, and you can add as many as
|
||||
you like. While it is not recommended to add the same meta parameter more than
|
||||
once, it does not currently cause an error, and even though the result of doing
|
||||
so is officially undefined, it will currently take the last specified value.
|
||||
|
||||
You may also specify a single meta parameter struct. This is useful if you'd
|
||||
like to reuse a value, or build a combined value programmatically. For example:
|
||||
|
||||
```mcl
|
||||
file "/tmp/f1" {
|
||||
content => "hello!\n",
|
||||
|
||||
Meta => $b ?: struct{
|
||||
noop => false,
|
||||
retry => -1,
|
||||
delay => 0,
|
||||
poll => 5,
|
||||
limit => 4.2,
|
||||
burst => 3,
|
||||
sema => ["foo:1", "bar:3",],
|
||||
autoedge => true,
|
||||
autogroup => false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Remember that the top-level `Meta` field supports the elvis operator, while the
|
||||
individual struct fields in the struct type do not. This is to be expected, but
|
||||
since they are syntactically similar, it is worth mentioning to avoid confusion.
|
||||
|
||||
Please note that at the moment, you must specify a full metaparams struct, since
|
||||
partial struct types are currently not supported in the language. Patches are
|
||||
welcome if you'd like to add this tricky feature!
|
||||
|
||||
##### Resource naming
|
||||
|
||||
Each resource must have a unique name of type `str` that is used to uniquely
|
||||
identify that resource, and can be used in the functioning of the resource at
|
||||
that resources discretion. For example, the `file` resource uses the unique name
|
||||
value to specify the path.
|
||||
|
||||
Alternatively, the name value may be a list of strings `[]str` to build a list
|
||||
of resources, each with a name from that list. When this is done, each resource
|
||||
will use the same set of parameters. The list of internal edges specified in the
|
||||
same resource block is created intelligently to have the appropriate edge for
|
||||
each separate resource.
|
||||
|
||||
Using this construct is a veiled form of looping (iteration). This technique is
|
||||
one of many ways you can perform iterative tasks that you might have
|
||||
traditionally used a `for` loop for instead. This is preferred, because flow
|
||||
control is error-prone and can make for less readable code.
|
||||
|
||||
##### Internal edges
|
||||
|
||||
Resources may also declare edges internally. The edges may point to or from
|
||||
another resource, and may optionally include a notification. The four properties
|
||||
are: `Before`, `Depend`, `Notify` and `Listen`. The first two represent normal
|
||||
@@ -171,7 +293,7 @@ send notifications. You may have multiples of these per resource, including
|
||||
multiple `Depend` lines if necessary. Each of these properties also supports the
|
||||
conditional inclusion `elvis` operator as well.
|
||||
|
||||
For example, you may write is:
|
||||
For example, you may write:
|
||||
|
||||
```mcl
|
||||
$b = true # for example purposes
|
||||
@@ -215,6 +337,122 @@ to express a relationship between three resources. The first character in the
|
||||
resource kind must be capitalized so that the parser can't ascertain
|
||||
unambiguously that we are referring to a dependency relationship.
|
||||
|
||||
#### Class
|
||||
|
||||
A class is a grouping structure that bind's a list of statements to a name in
|
||||
the scope where it is defined. It doesn't directly produce any output. To
|
||||
produce output it must be called via the `include` statement.
|
||||
|
||||
Defining classes follows the same scoping and shadowing rules that is applied to
|
||||
the `bind` statement, although they exist in a separate namespace. In other
|
||||
words you can have a variable named `foo` and a class named `foo` in the same
|
||||
scope without any conflicts.
|
||||
|
||||
Classes can be both parameterized or naked. If a parameterized class is defined,
|
||||
then the argument types must be either specified manually, or inferred with the
|
||||
type unification algorithm. One interesting property is that the same class
|
||||
definition can be used with `include` via two different input signatures,
|
||||
although in practice this is probably fairly rare. Some usage examples include:
|
||||
|
||||
A naked class definition:
|
||||
|
||||
```mcl
|
||||
class foo {
|
||||
# some statements go here
|
||||
}
|
||||
```
|
||||
|
||||
A parameterized class with both input types being inferred if possible:
|
||||
|
||||
```mcl
|
||||
class bar($a, $b) {
|
||||
# some statements go here
|
||||
}
|
||||
```
|
||||
|
||||
A parameterized class with one type specified statically and one being inferred:
|
||||
|
||||
```mcl
|
||||
class baz($a str, $b) {
|
||||
# some statements go here
|
||||
}
|
||||
```
|
||||
|
||||
Classes can also be nested within other classes. Here's a contrived example:
|
||||
|
||||
```mcl
|
||||
import "fmt"
|
||||
class c1($a, $b) {
|
||||
# nested class definition
|
||||
class c2($c) {
|
||||
test $a {
|
||||
stringptr => fmt.printf("%s is %d", $b, $c),
|
||||
}
|
||||
}
|
||||
|
||||
if $a == "t1" {
|
||||
include c2(42)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Defining polymorphic classes was considered but is not currently allowed at this
|
||||
time.
|
||||
|
||||
Recursive classes are not currently supported and it is not clear if they will
|
||||
be in the future. Discussion about this topic is welcome on the mailing list.
|
||||
|
||||
#### Include
|
||||
|
||||
The `include` statement causes the previously defined class to produce the
|
||||
contained output. This statement must be called with parameters if the named
|
||||
class is defined with those.
|
||||
|
||||
The defined class can be called as many times as you'd like either within the
|
||||
same scope or within different scopes. If a class uses inferred type input
|
||||
parameters, then the same class can even be called with different signatures.
|
||||
Whether the output is useful and whether there is a unique type unification
|
||||
solution is dependent on your code.
|
||||
|
||||
#### Import
|
||||
|
||||
The `import` statement imports a scope into the specified namespace. A scope can
|
||||
contain variable, class, and function definitions. All are statements.
|
||||
Furthermore, since each of these have different logical uses, you could
|
||||
theoretically import a scope that contains an `int` variable named `foo`, a
|
||||
class named `foo`, and a function named `foo` as well. Keep in mind that
|
||||
variables can contain functions (they can have a type of function) and are
|
||||
commonly called lambdas.
|
||||
|
||||
There are a few different kinds of imports. They differ by the string contents
|
||||
that you specify. Short single word, or multiple-word tokens separated by zero
|
||||
or more slashes are system imports. Eg: `math`, `fmt`, or even `math/trig`.
|
||||
Local imports are path imports that are relative to the current directory. They
|
||||
can either import a single `mcl` file, or an entire well-formed module. Eg:
|
||||
`file1.mcl` or `dir1/`. Lastly, you can have a remote import. This must be an
|
||||
absolute path to a well-formed module. The common transport is `git`, and it can
|
||||
be represented via an FQDN. Eg: `git://github.com/purpleidea/mgmt-example1/`.
|
||||
|
||||
The namespace that any of these are imported into depends on how you use the
|
||||
import statement. By default, each kind of import will have a logic namespace
|
||||
identifier associated with it. System imports use the last token in their name.
|
||||
Eg: `fmt` would be imported as `fmt` and `math/trig` would be imported as
|
||||
`trig`. Local imports do the same, except the required `.mcl` extension, or
|
||||
trailing slash are removed. Eg: `foo/file1.mcl` would be imported as `file1` and
|
||||
`bar/baz/` would be imported as `baz`. Remote imports use some more complex
|
||||
rules. In general, well-named modules that contain a final directory name in the
|
||||
form: `mgmt-whatever/` will be named `whatever`. Otherwise, the last path token
|
||||
will be converted to lowercase and the dashes will be converted to underscores.
|
||||
The rules for remote imports might change, and should not be considered stable.
|
||||
|
||||
In any of the import cases, you can change the namespace that you're imported
|
||||
into. Simply add the `as whatever` text at the end of the import, and `whatever`
|
||||
will be the name of the namespace. Please note that `whatever` is not surrounded
|
||||
by quotes, since it is an identifier, and not a `string`. If you'd like to add
|
||||
all of the import contents into the top-level scope, you can use the `as *` text
|
||||
to dump all of the contents in. This is generally not recommended, as it might
|
||||
cause a conflict with another identifier.
|
||||
|
||||
### Stages
|
||||
|
||||
The mgmt compiler runs in a number of stages. In order of execution they are:
|
||||
@@ -273,6 +511,9 @@ without making any changes. The `ExprVar` node naturally consumes scope's and
|
||||
the `StmtProg` node cleverly passes the scope through in the order expected for
|
||||
the out-of-order bind logic to work.
|
||||
|
||||
This step typically calls the ordering algorithm to determine the correct order
|
||||
of statements in a program.
|
||||
|
||||
#### Type unification
|
||||
|
||||
Each expression must have a known type. The unpleasant option is to force the
|
||||
@@ -596,6 +837,39 @@ someListOfStrings := &types.ListValue{
|
||||
If you don't build these properly, then you will cause a panic! Even empty lists
|
||||
have a type.
|
||||
|
||||
### Is the `class` statement a singleton?
|
||||
|
||||
Not really, but practically it can be used as such. The `class` statement is not
|
||||
a singleton since it can be called multiple times in different locations, and it
|
||||
can also be parameterized and called multiple times (with `include`) using
|
||||
different input parameters. The reason it can be used as such is that statement
|
||||
output (from multple classes) that is compatible (and usually identical) will
|
||||
be automatically collated and have the duplicates removed. In that way, you can
|
||||
assume that an unparameterized class is always a singleton, and that
|
||||
parameterized classes can often be singletons depending on their contents and if
|
||||
they are called in an identical way or not. In reality the de-duplication
|
||||
actually happens at the resource output level, so anything that produces
|
||||
multiple compatible resources is allowed.
|
||||
|
||||
### Are recursive `class` definitions supported?
|
||||
|
||||
Recursive class definitions where the contents of a `class` contain a
|
||||
self-referential `include`, either directly, or with indirection via any other
|
||||
number of classes is not supported. It's not clear if it ever will be in the
|
||||
future, unless we decide it's worth the extra complexity. The reason is that our
|
||||
FRP actually generates a static graph which doesn't change unless the code does.
|
||||
To support dynamic graphs would require our FRP to be a "higher-order" FRP,
|
||||
instead of the simpler "first-order" FRP that it is now. You might want to
|
||||
verify that I got the [nomenclature](https://github.com/gelisam/frp-zoo)
|
||||
correct. If it turns out that there's an important advantage to supporting a
|
||||
higher-order FRP in mgmt, then we can consider that in the future.
|
||||
|
||||
I realized that recursion would require a static graph when I considered the
|
||||
structure required for a simple recursive class definition. If some "depth"
|
||||
value wasn't known statically by compile time, then there would be no way to
|
||||
know how large the graph would grow, and furthermore, the graph would need to
|
||||
change if that "depth" value changed.
|
||||
|
||||
### I don't like the mgmt language, is there an alternative?
|
||||
|
||||
Yes, the language is just one of the available "frontends" that passes a stream
|
||||
|
||||
@@ -34,7 +34,7 @@ if we missed something that you think is relevant!
|
||||
| James Shubin | video | [Recording from Incontro DevOps 2017](https://vimeo.com/212241877) |
|
||||
| Yves Brissaud | blog | [mgmt aux HumanTalks Grenoble (french)](http://log.winsos.net/2017/04/12/mgmt-aux-human-talks-grenoble.html) |
|
||||
| James Shubin | video | [Recording from OSDC Berlin 2017](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1) |
|
||||
| Jonathan Gold | blog | [AWS:EC2 in mgmt](http://jonathangold.ca/awsec2-in-mgmt/) |
|
||||
| Jonathan Gold | blog | [AWS:EC2 in mgmt](https://jonathangold.ca/blog/aws-ec2-in-mgmt/) |
|
||||
| James Shubin | video | [Recording from OSMC Nuremberg 2017](https://www.youtube.com/watch?v=hSVadQLeplU&html5=1) |
|
||||
| James Shubin | video | [Recording from LCA 2018, Developers Miniconf](https://www.youtube.com/watch?v=OvgGfW0ilbE) |
|
||||
| James Shubin | video | [Recording from LCA 2018, Sysadmin Miniconf](https://www.youtube.com/watch?v=ELq1XOJMIPY) |
|
||||
@@ -43,3 +43,16 @@ if we missed something that you think is relevant!
|
||||
| James Shubin | video | [Recording from FOSDEM 2018, Config Management Devroom](https://video.fosdem.org/2018/UA2.114/mgmt.webm) |
|
||||
| James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) |
|
||||
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) |
|
||||
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |
|
||||
| James Shubin | video | [Recording from DevOpsDays Montreal 2018](https://www.youtube.com/watch?v=1i38c5cooHo) |
|
||||
| James Shubin | video | [Recording from FOSDEM Minimalistic Languages Devroom 2019](https://video.fosdem.org/2019/K.4.201/mgmtconfig.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM Infra Management Devroom 2019](https://video.fosdem.org/2019/UB2.252A/mgmt.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM Graph Processing Devroom 2019](https://video.fosdem.org/2019/H.1308/graph_mgmt_config.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM Virtualization Devroom 2019](https://video.fosdem.org/2019/H.2213/vai_real_time_virtualization_automation.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM Containers Devroom 2019](https://video.fosdem.org/2019/UA2.114/containers_mgmt.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM Monitoring Devroom 2019](https://video.fosdem.org/2019/UB2.252A/real_time_merging_of_config_management_and_monitoring.webm) |
|
||||
| James Shubin | blog | [Mgmt Configuration Language: Class and Include](https://purpleidea.com/blog/2019/07/26/class-and-include-in-mgmt/) |
|
||||
| James Shubin | video | [Recording from FOSDEM 2020, Main Track (History)](https://video.fosdem.org/2020/Janson/automation.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM 2020, Infra Management Devroom](https://video.fosdem.org/2020/UA2.120/mgmt.webm) |
|
||||
| James Shubin | video | [Recording from FOSDEM 2020, Minimalistic Languages Devroom](https://video.fosdem.org/2020/AW1.125/mgmtconfigmore.webm) |
|
||||
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2020](https://www.youtube.com/watch?v=Kd7FAORFtsc) |
|
||||
|
||||
@@ -143,7 +143,7 @@ you to specify which `puppet.conf` file should be used during
|
||||
translation.
|
||||
|
||||
```
|
||||
mgmt run --puppet /opt/my-manifest.pp --puppet-conf /etc/mgmt/puppet.conf
|
||||
mgmt run puppet --puppet /opt/my-manifest.pp --puppet-conf /etc/mgmt/puppet.conf
|
||||
```
|
||||
|
||||
Within this file, you can just specify any needed options in the
|
||||
@@ -164,3 +164,152 @@ language features.
|
||||
You should probably make sure to always use the latest release of
|
||||
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
|
||||
getting pulled in as a dependency of the former).
|
||||
|
||||
## Using Puppet in conjunction with the mcl lang
|
||||
|
||||
The graph that Puppet generates for `mgmt` can be united with a graph
|
||||
that is created from native `mgmt` code in its mcl language. This is
|
||||
useful when you are in the process of replacing Puppet with mgmt. You
|
||||
can translate your custom modules into mgmt's language one by one,
|
||||
and let mgmt run the current mix.
|
||||
|
||||
Instead of the usual `--puppet-conf` flag and argv for `puppet` and `mcl` input,
|
||||
you need to use alternative flags to make this work:
|
||||
|
||||
* `--lp-lang` to specify the mcl input
|
||||
* `--lp-puppet` to specify the puppet input
|
||||
* `--lp-puppet-conf` to point to the optional puppet.conf file
|
||||
|
||||
`mgmt` will derive a graph that contains all edges and vertices from
|
||||
both inputs. You essentially get two unrelated subgraphs that run in
|
||||
parallel. To form edges between these subgraphs, you have to define
|
||||
special vertices that will be merged. This works through a hard-coded
|
||||
naming scheme.
|
||||
|
||||
### Mixed graph example 1 - No merges
|
||||
|
||||
```mcl
|
||||
# lang
|
||||
file "/tmp/mgmt_dir/" { state => "present" }
|
||||
file "/tmp/mgmt_dir/a" { state => "present" }
|
||||
```
|
||||
|
||||
```puppet
|
||||
# puppet
|
||||
file { "/tmp/puppet_dir": ensure => "directory" }
|
||||
file { "/tmp/puppet_dir/a": ensure => "file" }
|
||||
```
|
||||
|
||||
These very simple inputs (including implicit edges from directory to
|
||||
respective file) result in two subgraphs that do not relate.
|
||||
|
||||
```
|
||||
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
|
||||
|
||||
File[/tmp/puppet_dir] -> File[/tmp/puppet_dir/a]
|
||||
```
|
||||
|
||||
### Mixed graph example 2 - Merged vertex
|
||||
|
||||
In order to have merged vertices in the resulting graph, you will
|
||||
need to include special resources and classes in the respective
|
||||
input code.
|
||||
|
||||
* On the lang side, add `noop` resources with names starting in `puppet_`.
|
||||
* On the Puppet side, add **empty** classes with names starting in `mgmt_`.
|
||||
|
||||
```mcl
|
||||
# lang
|
||||
noop "puppet_handover_to_mgmt" {}
|
||||
file "/tmp/mgmt_dir/" { state => "present" }
|
||||
file "/tmp/mgmt_dir/a" { state => "present" }
|
||||
|
||||
Noop["puppet_handover_to_mgmt"] -> File["/tmp/mgmt_dir/"]
|
||||
```
|
||||
|
||||
```puppet
|
||||
# puppet
|
||||
class mgmt_handover_to_mgmt {}
|
||||
include mgmt_handover_to_mgmt
|
||||
|
||||
file { "/tmp/puppet_dir": ensure => "directory" }
|
||||
file { "/tmp/puppet_dir/a": ensure => "file" }
|
||||
|
||||
File["/tmp/puppet_dir/a"] -> Class["mgmt_handover_to_mgmt"]
|
||||
```
|
||||
|
||||
The new `noop` resource is merged with the new class, resulting in
|
||||
the following graph:
|
||||
|
||||
```
|
||||
File[/tmp/puppet_dir] -> File[/tmp/puppet_dir/a]
|
||||
|
|
||||
V
|
||||
Noop[handover_to_mgmt]
|
||||
|
|
||||
V
|
||||
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
|
||||
```
|
||||
|
||||
You put all your ducks in a row, and the resources from the Puppet input
|
||||
run before those from the mcl input.
|
||||
|
||||
**Note:** The names of the `noop` and the class must be identical after the
|
||||
respective prefix. The common part (here, `handover_to_mgmt`) becomes the name
|
||||
of the merged resource.
|
||||
|
||||
## Mixed graph example 3 - Multiple merges
|
||||
|
||||
In most scenarios, it will not be possible to define a single handover
|
||||
point like in the previous example. For example, if some Puppet resources
|
||||
need to run in between two stages of native resources, you need at least
|
||||
two merged vertices:
|
||||
|
||||
```mcl
|
||||
# lang
|
||||
noop "puppet_handover" {}
|
||||
noop "puppet_handback" {}
|
||||
file "/tmp/mgmt_dir/" { state => "present" }
|
||||
file "/tmp/mgmt_dir/a" { state => "present" }
|
||||
file "/tmp/mgmt_dir/puppet_subtree/state-file" { state => "present" }
|
||||
|
||||
File["/tmp/mgmt_dir/"] -> Noop["puppet_handover"]
|
||||
Noop["puppet_handback"] -> File["/tmp/mgmt_dir/puppet_subtree/state-file"]
|
||||
```
|
||||
|
||||
```puppet
|
||||
# puppet
|
||||
class mgmt_handover {}
|
||||
class mgmt_handback {}
|
||||
|
||||
include mgmt_handover, mgmt_handback
|
||||
|
||||
class important_stuff {
|
||||
file { "/tmp/mgmt_dir/puppet_subtree":
|
||||
ensure => "directory"
|
||||
}
|
||||
# ...
|
||||
}
|
||||
|
||||
Class["mgmt_handover"] -> Class["important_stuff"] -> Class["mgmt_handback"]
|
||||
```
|
||||
|
||||
The resulting graph looks roughly like this:
|
||||
|
||||
```
|
||||
File[/tmp/mgmt_dir/] -> File[/tmp/mgmt_dir/a]
|
||||
|
|
||||
V
|
||||
Noop[handover] -> ( class important_stuff resources )
|
||||
|
|
||||
V
|
||||
Noop[handback]
|
||||
|
|
||||
V
|
||||
File[/tmp/mgmt_dir/puppet_subtree/state-file]
|
||||
```
|
||||
|
||||
You can add arbitrary numbers of merge pairs to your code bases,
|
||||
with relationships as needed. From our limited experience, code
|
||||
readability suffers quite a lot from these, however. We advise
|
||||
to keep these structures simple.
|
||||
|
||||
@@ -2,65 +2,108 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
This guide is intended for developers. Once `mgmt` is minimally viable, we'll
|
||||
publish a quick start guide for users too. If you're brand new to `mgmt`, it's
|
||||
probably a good idea to start by reading the
|
||||
[introductory article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||
or to watch an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1).
|
||||
Once you're familiar with the general idea, please start hacking...
|
||||
This guide is intended for users and developers. If you're brand new to `mgmt`,
|
||||
it's probably a good idea to start by reading an
|
||||
[introductory article about the engine](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||
and an [introductory article about the language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/).
|
||||
[There are other articles and videos available](on-the-web.md) if you'd like to
|
||||
learn more or prefer different formats. Once you're familiar with the general
|
||||
idea, or if you prefer a hands-on approach, please start hacking...
|
||||
|
||||
## Quick start
|
||||
## Getting mgmt
|
||||
|
||||
### Installing golang
|
||||
You can either build `mgmt` from source, or you can download a pre-built
|
||||
release. There are also some distro repositories available, but they may not be
|
||||
up to date. A pre-built release is the fastest option if there's one that's
|
||||
available for your platform. If you are developing or testing a new patch to
|
||||
`mgmt`, or there is not a release available for your platform, then you'll have
|
||||
to build your own.
|
||||
|
||||
* You need golang version 1.9 or greater installed.
|
||||
### Downloading a pre-built release:
|
||||
|
||||
The latest releases can be found [here](https://github.com/purpleidea/mgmt/releases/).
|
||||
An alternate mirror is available [here](https://dl.fedoraproject.org/pub/alt/purpleidea/mgmt/releases/).
|
||||
|
||||
Make sure to verify the signatures of all packages before you use them. The
|
||||
signing key can be downloaded from [https://purpleidea.com/contact/#pgp-key](https://purpleidea.com/contact/#pgp-key)
|
||||
to verify the release.
|
||||
|
||||
If you've decided to install a pre-build release, you can skip to the
|
||||
[Running mgmt](#running-mgmt) section below!
|
||||
|
||||
### Building a release:
|
||||
|
||||
You'll need some dependencies, including `golang`, and some associated tools.
|
||||
|
||||
#### Installing golang
|
||||
|
||||
* You need golang version 1.13 or greater installed.
|
||||
* To install on rpm style systems: `sudo dnf install golang`
|
||||
* To install on apt style systems: `sudo apt install golang`
|
||||
* To install on macOS systems install [Homebrew](https://brew.sh)
|
||||
and run: `brew install go`
|
||||
* You can run `go version` to check the golang version.
|
||||
* If your distro is tool old, you may need to [download](https://golang.org/dl/)
|
||||
* If your distro is too old, you may need to [download](https://golang.org/dl/)
|
||||
a newer golang version.
|
||||
|
||||
### Setting up golang
|
||||
#### Setting up golang
|
||||
|
||||
* If you do not have a GOPATH yet, create one and export it:
|
||||
* You can skip this step, as your installation will default to using `~/go/`,
|
||||
but if you do not have a `GOPATH` yet and want one in a custom location, create
|
||||
one and export it:
|
||||
|
||||
```
|
||||
```shell
|
||||
mkdir $HOME/gopath
|
||||
export GOPATH=$HOME/gopath
|
||||
```
|
||||
|
||||
* You might also want to add the GOPATH to your `~/.bashrc` or `~/.profile`.
|
||||
* For more information you can read the [GOPATH documentation](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable).
|
||||
* For more information you can read the
|
||||
[GOPATH documentation](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable).
|
||||
|
||||
### Getting the mgmt code and dependencies
|
||||
#### Getting the mgmt code and associated dependencies
|
||||
|
||||
* Download the `mgmt` code into the GOPATH, and switch to that directory:
|
||||
* Download the `mgmt` code into the `GOPATH`, and switch to that directory:
|
||||
|
||||
```
|
||||
mkdir -p $GOPATH/src/github.com/purpleidea/
|
||||
cd $GOPATH/src/github.com/purpleidea/
|
||||
```shell
|
||||
[ -z "$GOPATH" ] && mkdir ~/go/ || mkdir -p $GOPATH/src/github.com/purpleidea/
|
||||
cd $GOPATH/src/github.com/purpleidea/ || cd ~/go/
|
||||
git clone --recursive https://github.com/purpleidea/mgmt/
|
||||
cd $GOPATH/src/github.com/purpleidea/mgmt
|
||||
cd $GOPATH/src/github.com/purpleidea/mgmt/ || cd ~/go/src/github.com/purpleidea/mgmt/
|
||||
```
|
||||
|
||||
* Add $GOPATH/bin to $PATH
|
||||
* Add `$GOPATH/bin` to `$PATH`
|
||||
|
||||
```
|
||||
```shell
|
||||
export PATH=$PATH:$GOPATH/bin
|
||||
```
|
||||
|
||||
* Run `make deps` to install system and golang dependencies. Take a look at
|
||||
`misc/make-deps.sh` for details.
|
||||
* Run `make build` to get a freshly built `mgmt` binary.
|
||||
`misc/make-deps.sh` if you want to see the details of what it does.
|
||||
|
||||
### Running mgmt
|
||||
#### Building mgmt
|
||||
|
||||
* Run `time ./mgmt run --lang examples/lang/hello0.mcl --tmp-prefix` to try out
|
||||
a very simple example!
|
||||
* Now run `make` to get a freshly built `mgmt` binary. If this succeeds, you can
|
||||
proceed to the [Running mgmt](#running-mgmt) section below!
|
||||
|
||||
### Installing a distro release
|
||||
|
||||
Installation of `mgmt` from distribution packages currently needs improvement.
|
||||
They are not always up-to-date with git master and as such are not recommended.
|
||||
At the moment we have:
|
||||
* [COPR](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/) (currently dead)
|
||||
* [Arch](https://aur.archlinux.org/packages/mgmt/) (currently stale)
|
||||
|
||||
Please contribute more and help improve these! We'd especially like to see a
|
||||
Debian package!
|
||||
|
||||
## Running mgmt
|
||||
|
||||
* Run `mgmt run --tmp-prefix lang examples/lang/hello0.mcl` to try out a very
|
||||
simple example! If you built it from source, you'll need to use `./mgmt` from
|
||||
the project directory.
|
||||
* Look in that example file that you ran to see if you can figure out what it
|
||||
did!
|
||||
did! You can press `^C` to exit `mgmt`.
|
||||
* Have fun hacking on our future technology and get involved to shape the
|
||||
project!
|
||||
|
||||
@@ -68,115 +111,3 @@ project!
|
||||
|
||||
Please look in the [examples/lang/](../examples/lang/) folder for some more
|
||||
examples!
|
||||
|
||||
## Vagrant
|
||||
|
||||
If you would like to avoid doing the above steps manually, we have prepared a
|
||||
[Vagrant](https://www.vagrantup.com/) environment for your convenience. From the
|
||||
project directory, run a `vagrant up`, and then a `vagrant status`. From there,
|
||||
you can `vagrant ssh` into the `mgmt` machine. The MOTD will explain the rest.
|
||||
|
||||
## Using Docker
|
||||
|
||||
Alternatively, you can check out the [docker-guide](docs/docker-guide.md) in
|
||||
order to develop or deploy using docker.
|
||||
|
||||
## Information about dependencies
|
||||
|
||||
Software projects have a few different kinds of dependencies. There are _build_
|
||||
dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
|
||||
required for running the _test_ suite.
|
||||
|
||||
### Build
|
||||
|
||||
* `golang` 1.9 or higher (required, available in some distros and distributed
|
||||
as a binary officially by [golang.org](https://golang.org/dl/))
|
||||
|
||||
### Runtime
|
||||
|
||||
A relatively modern GNU/Linux system should be able to run `mgmt` without any
|
||||
problems. Since `mgmt` runs as a single statically compiled binary, all of the
|
||||
library dependencies are included. It is expected, that certain advanced
|
||||
resources require host specific facilities to work. These requirements are
|
||||
listed below:
|
||||
|
||||
| Resource | Dependency | Version | Check version with |
|
||||
|----------|-------------------|-----------------------------|-----------------------------------------------------------|
|
||||
| augeas | augeas-devel | `augeas 1.6` or greater | `dnf info augeas-devel` or `apt-cache show libaugeas-dev` |
|
||||
| file | inotify | `Linux 2.6.27` or greater | `uname -a` |
|
||||
| hostname | systemd-hostnamed | `systemd 25` or greater | `systemctl --version` |
|
||||
| nspawn | systemd-nspawn | `systemd ???` or greater | `systemctl --version` |
|
||||
| pkg | packagekitd | `packagekit 1.x` or greater | `pkcon --version` |
|
||||
| svc | systemd | `systemd ???` or greater | `systemctl --version` |
|
||||
| virt | libvirt-devel | `libvirt 1.2.0` or greater | `dnf info libvirt-devel` or `apt-cache show libvirt-dev` |
|
||||
| virt | libvirtd | `libvirt 1.2.0` or greater | `libvirtd --version` |
|
||||
|
||||
For building a visual representation of the graph, `graphviz` is required.
|
||||
|
||||
To build `mgmt` without augeas support please run:
|
||||
`GOTAGS='noaugeas' make build`
|
||||
|
||||
To build `mgmt` without libvirt support please run:
|
||||
`GOTAGS='novirt' make build`
|
||||
|
||||
To build `mgmt` without augeas or libvirt support please run:
|
||||
`GOTAGS='noaugeas novirt' make build`
|
||||
|
||||
## Binary Package Installation
|
||||
|
||||
Installation of `mgmt` from distribution packages currently needs improvement.
|
||||
They are not always up-to-date with git master and as such are not recommended.
|
||||
At the moment we have:
|
||||
* [COPR](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||
* [Arch](https://aur.archlinux.org/packages/mgmt/)
|
||||
|
||||
Please contribute more! We'd especially like to see a Debian package!
|
||||
|
||||
## OSX/macOS/Darwin development
|
||||
|
||||
Developing and running `mgmt` on macOS is currently not supported (but not
|
||||
discouraged either). Meaning it might work but in the case it doesn't you would
|
||||
have to provide your own patches to fix problems (the project maintainer and
|
||||
community are glad to assist where needed).
|
||||
|
||||
There are currently some issues that make `mgmt` less suitable to run for provisioning
|
||||
macOS. But as a client to provision remote servers it should run fine.
|
||||
|
||||
Since the primary supported systems are Linux and these are the environments
|
||||
tested for it is wise to run these suites during macOS development as well. To
|
||||
ease this Docker can be leveraged ([Docker for Mac](https://docs.docker.com/docker-for-mac/)).
|
||||
|
||||
Before running any of the commands below create the development Docker image:
|
||||
|
||||
```
|
||||
docker/scripts/build-development
|
||||
```
|
||||
|
||||
This image requires updating every time dependencies (`make-deps.sh`) change.
|
||||
|
||||
Then to run the test suite:
|
||||
|
||||
```
|
||||
docker run --rm -ti \
|
||||
-v $PWD:/go/src/github.com/purpleidea/mgmt/ \
|
||||
-w /go/src/github.com/purpleidea/mgmt/ \
|
||||
purpleidea/mgmt:development \
|
||||
make test
|
||||
```
|
||||
|
||||
For convenience this command is wrapped in `docker/scripts/exec-development`.
|
||||
|
||||
Basically any command can be executed this way. Because the repository source is
|
||||
mounted into the Docker container invocation will be quick and allow rapid
|
||||
testing, example:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development test/test-shell.sh load0.sh
|
||||
```
|
||||
|
||||
Other examples:
|
||||
|
||||
```
|
||||
docker/scripts/exec-development make build
|
||||
docker/scripts/exec-development ./mgmt run --tmp-prefix --lang examples/lang/load0.mcl
|
||||
```
|
||||
|
||||
@@ -19,28 +19,93 @@ on this design, please read the
|
||||
[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||
on the subject.
|
||||
|
||||
## Resource Prerequisites
|
||||
|
||||
### Imports
|
||||
|
||||
You'll need to import a few packages to make writing your resource easier. Here
|
||||
is the list:
|
||||
|
||||
```
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
```
|
||||
|
||||
The `engine` package contains most of the interfaces and helper functions that
|
||||
you'll need to use. The `traits` package contains some base functionality which
|
||||
you can use to easily add functionality to your resource without needing to
|
||||
implement it from scratch.
|
||||
|
||||
### Resource struct
|
||||
|
||||
Each resource will implement methods as pointer receivers on a resource struct.
|
||||
The naming convention for resources is that they end with a `Res` suffix.
|
||||
|
||||
The resource struct should include an anonymous reference to the `Base` trait.
|
||||
Other `traits` can be added to the resource to add additional functionality.
|
||||
They are discussed below.
|
||||
|
||||
You'll most likely want to store a reference to the `*Init` struct type as
|
||||
defined by the engine. This is data that the engine will provide to your
|
||||
resource on Init.
|
||||
|
||||
Lastly you should define the public fields that make up your resource API, as
|
||||
well as any private fields that you might want to use throughout your resource.
|
||||
Do _not_ depend on global variables, since multiple copies of your resource
|
||||
could get instantiated.
|
||||
|
||||
You'll want to add struct tags based on the different frontends that you want
|
||||
your resources to be able to use. Some frontends can infer this information if
|
||||
it is not specified, but others cannot, and some might poorly infer if the
|
||||
struct name is ambiguous.
|
||||
|
||||
If you'd like your resource to be accessible by the `YAML` graph API (GAPI),
|
||||
then you'll need to include the appropriate YAML fields as shown below. This is
|
||||
used by the `Puppet` compiler as well, so make sure you include these struct
|
||||
tags if you want existing `Puppet` code to be able to run using the `mgmt`
|
||||
engine.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
type FooRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Groupable
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Whatever string `lang:"whatever" yaml:"whatever"` // you pick!
|
||||
Baz bool `lang:"baz" yaml:"baz"` // something else
|
||||
|
||||
something string // some private field
|
||||
}
|
||||
```
|
||||
|
||||
## Resource API
|
||||
|
||||
To implement a resource in `mgmt` it must satisfy the
|
||||
[`Res`](https://github.com/purpleidea/mgmt/blob/master/resources/resources.go)
|
||||
[`Res`](https://github.com/purpleidea/mgmt/blob/master/engine/resources.go)
|
||||
interface. What follows are each of the method signatures and a description of
|
||||
each.
|
||||
|
||||
### Default
|
||||
|
||||
```golang
|
||||
Default() Res
|
||||
Default() engine.Res
|
||||
```
|
||||
|
||||
This returns a populated resource struct as a `Res`. It shouldn't populate any
|
||||
values which already have the correct default as the golang zero value. In
|
||||
values which already get a good default as the respective golang zero value. In
|
||||
general it is preferable if the zero values make for the correct defaults.
|
||||
(This is to say, resources are designed to behave safely and intuitively
|
||||
when parameters take a zero value, whenever this is possible.)
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *FooRes) Default() Res {
|
||||
func (obj *FooRes) Default() engine.Res {
|
||||
return &FooRes{
|
||||
Answer: 42, // sometimes, defaults shouldn't be the zero value
|
||||
}
|
||||
@@ -55,9 +120,12 @@ Validate() error
|
||||
|
||||
This method is used to validate if the populated resource struct is a valid
|
||||
representation of the resource kind. If it does not conform to the resource
|
||||
specifications, it should generate an error. If you notice that this method is
|
||||
specifications, it should return an error. If you notice that this method is
|
||||
quite large, it might be an indication that you should reconsider the parameter
|
||||
list and interface to this resource. This method is called _before_ `Init`.
|
||||
list and interface to this resource. This method is called by the engine
|
||||
_before_ `Init`. It can also be called occasionally after a Send/Recv operation
|
||||
to verify that the newly populated parameters are valid. Remember not to expect
|
||||
access to the outside world when using this.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -67,7 +135,7 @@ func (obj *FooRes) Validate() error {
|
||||
if obj.Answer != 42 { // validate whatever you want
|
||||
return fmt.Errorf("expected an answer of 42")
|
||||
}
|
||||
return obj.BaseRes.Validate() // remember to call the base method!
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -78,19 +146,28 @@ Init() error
|
||||
```
|
||||
|
||||
This is called to initialize the resource. If something goes wrong, it should
|
||||
return an error. It should do any resource specific work, and finish by calling
|
||||
the `Init` method of the base resource.
|
||||
return an error. It should do any resource specific work such as initializing
|
||||
channels, sync primitives, or anything else that is relevant to your resource.
|
||||
If it is not need throughout, it might be preferable to do some initialization
|
||||
and tear down locally in either the Watch method or CheckApply method. The
|
||||
choice depends on your particular resource and making the best decision requires
|
||||
some experience with mgmt. If you are unsure, feel free to ask an existing
|
||||
`mgmt` contributor. During `Init`, the engine will pass your resource a struct
|
||||
containing some useful data and pointers. You should save a copy of this pointer
|
||||
since you will need to use it in other parts of your resource.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Init initializes the Foo resource.
|
||||
func (obj *FooRes) Init() error {
|
||||
func (obj *FooRes) Init(init *engine.Init) error
|
||||
obj.init = init // save for later
|
||||
|
||||
// run the resource specific initialization, and error if anything fails
|
||||
if some_error {
|
||||
return err // something went wrong!
|
||||
}
|
||||
return obj.BaseRes.Init() // call the base resource init
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
@@ -108,7 +185,9 @@ Close() error
|
||||
|
||||
This is called to cleanup after the resource. It is usually not necessary, but
|
||||
can be useful if you'd like to properly close a persistent connection that you
|
||||
opened in the `Init` method and were using throughout the resource.
|
||||
opened in the `Init` method and were using throughout the resource. It is *not*
|
||||
the shutdown signal that tells the resource to exit. That happens in the Watch
|
||||
loop.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -116,21 +195,13 @@ opened in the `Init` method and were using throughout the resource.
|
||||
// Close runs some cleanup code for this resource.
|
||||
func (obj *FooRes) Close() error {
|
||||
err := obj.conn.Close() // close some internal connection
|
||||
|
||||
// call base close, b/c we're overriding
|
||||
if e := obj.BaseRes.Close(); err == nil {
|
||||
err = e
|
||||
} else if e != nil {
|
||||
err = multierr.Append(err, e) // list of errors
|
||||
}
|
||||
obj.someMap = nil // free up some large data structure from memory
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
You should probably check the return errors of your internal methods, and pass
|
||||
on an error if something went wrong. Remember to always call the base `Close`
|
||||
method! If you plan to return early if you hit an internal error, then at least
|
||||
call it with a defer!
|
||||
on an error if something went wrong.
|
||||
|
||||
### CheckApply
|
||||
|
||||
@@ -143,7 +214,8 @@ function should check if the state of this resource is correct, and if so, it
|
||||
should return: `(true, nil)`. If the `apply` variable is set to `true`, then
|
||||
this means that we should then proceed to run the changes required to bring the
|
||||
resource into the correct state. If the `apply` variable is set to `false`, then
|
||||
the resource is operating in _noop_ mode and _no operations_ should be executed!
|
||||
the resource is operating in _noop_ mode and _no operational changes_ should be
|
||||
made!
|
||||
|
||||
After having executed the necessary operations to bring the resource back into
|
||||
the desired state, or after having detected that the state was incorrect, but
|
||||
@@ -155,8 +227,8 @@ function. If you cannot, then you must return an error! The exception to this
|
||||
rule is that if an external force changes the state of the resource while it is
|
||||
being remedied, it is possible to return from this function even though the
|
||||
resource isn't now converged. This is not a bug, as the resources `Watch`
|
||||
facility will detect the change, ultimately resulting in a subsequent call to
|
||||
`CheckApply`.
|
||||
facility will detect the new change, ultimately resulting in a subsequent call
|
||||
to `CheckApply`.
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -165,11 +237,15 @@ facility will detect the change, ultimately resulting in a subsequent call to
|
||||
func (obj *FooRes) CheckApply(apply bool) (bool, error) {
|
||||
// check the state
|
||||
if state_is_okay { return true, nil } // done early! :)
|
||||
|
||||
// state was bad
|
||||
if !apply { return false, nil } // don't apply; !stateok, nil
|
||||
|
||||
if !apply { return false, nil } // don't apply, we're in noop mode
|
||||
|
||||
if any_error { return false, err } // anytime there's an err!
|
||||
|
||||
// do the apply!
|
||||
return false, nil // after success applying
|
||||
if any_error { return false, err } // anytime there's an err!
|
||||
}
|
||||
```
|
||||
|
||||
@@ -180,20 +256,6 @@ skipped. This is an engine optimization, and not a bug. It is mentioned here in
|
||||
the documentation in case you are confused as to why a debug message you've
|
||||
added to the code isn't always printed.
|
||||
|
||||
#### Refresh notifications
|
||||
|
||||
Some resources may choose to support receiving refresh notifications. In general
|
||||
these should be avoided if possible, but nevertheless, they do make sense in
|
||||
certain situations. Resources that support these need to verify if one was sent
|
||||
during the CheckApply phase of execution. This is accomplished by calling the
|
||||
`Refresh() bool` method of the resource, and inspecting the return value. This
|
||||
is only necessary if you plan to perform a refresh action. Refresh actions
|
||||
should still respect the `apply` variable, and no system changes should be made
|
||||
if it is `false`. Refresh notifications are generated by any resource when an
|
||||
action is applied by that resource and are transmitted through graph edges which
|
||||
have enabled their propagation. Resources that currently perform some refresh
|
||||
action include `svc`, `timer`, and `password`.
|
||||
|
||||
#### Paired execution
|
||||
|
||||
For many resources it is not uncommon to see `CheckApply` run twice in rapid
|
||||
@@ -210,7 +272,7 @@ will likely find the state to now be correct.
|
||||
* If the state is correct and no changes are needed, return `(true, nil)`.
|
||||
* You should only make changes to the system if `apply` is set to `true`.
|
||||
* After checking the state and possibly applying the fix, return `(false, nil)`.
|
||||
* Returning `(true, err)` is a programming error and will cause a `Fatal`.
|
||||
* Returning `(true, err)` is a programming error and can have a negative effect.
|
||||
|
||||
### Watch
|
||||
|
||||
@@ -223,7 +285,7 @@ state of the resource might have changed. To send a message you should write to
|
||||
the input event channel using the `Event` helper method. The Watch function
|
||||
should run continuously until a shutdown message is received. If at any time
|
||||
something goes wrong, you should return an error, and the `mgmt` engine will
|
||||
handle possibly restarting the main loop based on the `retry` meta parameters.
|
||||
handle possibly restarting the main loop based on the `retry` meta parameter.
|
||||
|
||||
It is better to send an event notification which turns out to be spurious, than
|
||||
to miss a possible event. Resources which can miss events are incorrect and need
|
||||
@@ -247,18 +309,18 @@ running.
|
||||
The lifetime of most resources `Watch` method should be spent in an infinite
|
||||
loop that is bounded by a `select` call. The `select` call is the point where
|
||||
our method hands back control to the engine (and the kernel) so that we can
|
||||
sleep until something of interest wakes us up. In this loop we must process
|
||||
events from the engine via the `<-obj.Events()` call, and receive events for our
|
||||
resource itself!
|
||||
sleep until something of interest wakes us up. In this loop we must wait until
|
||||
we get a shutdown event from the engine via the `<-obj.init.Done` channel, which
|
||||
closes when we'd like to shut everything down. At this point you should cleanup,
|
||||
and let `Watch` close.
|
||||
|
||||
#### Events
|
||||
|
||||
If we receive an internal event from the `<-obj.Events()` method, we can read it
|
||||
with the ReadEvent helper function. This function tells us if we should shutdown
|
||||
our resource, and if we should generate an event. When we want to send an event,
|
||||
we use the `Event` helper function. It is also important to mark the resource
|
||||
state as `dirty` if we believe it might have changed. We do this with the
|
||||
`StateOK(false)` function.
|
||||
If the `<-obj.init.Done` channel closes, we should shutdown our resource. When
|
||||
When we want to send an event, we use the `Event` helper function. This
|
||||
automatically marks the resource state as `dirty`. If you're unsure, it's not
|
||||
harmful to send the event. This will ultimately cause `CheckApply` to run. This
|
||||
method can block if the resource is being paused.
|
||||
|
||||
#### Startup
|
||||
|
||||
@@ -266,22 +328,16 @@ Once the `Watch` function has finished starting up successfully, it is important
|
||||
to generate one event to notify the `mgmt` engine that we're now listening
|
||||
successfully, so that it can run an initial `CheckApply` to ensure we're safely
|
||||
tracking a healthy state and that we didn't miss anything when `Watch` was down
|
||||
or from before `mgmt` was running. It does this by calling the `Running` method.
|
||||
or from before `mgmt` was running. You must do this by calling the
|
||||
`obj.init.Running` method.
|
||||
|
||||
#### Converged
|
||||
|
||||
The engine might be asked to shutdown when the entire state of the system has
|
||||
not seen any changes for some duration of time. The engine can determine this
|
||||
automatically, but each resource can block this if it is absolutely necessary.
|
||||
To do this, the `Watch` method should get the `ConvergedUID` handle that has
|
||||
been prepared for it by the engine. This is done by calling the `ConvergerUID`
|
||||
method on the resource object. The result can be used to set the converged
|
||||
status with `SetConverged`, and to notify when the particular timeout has been
|
||||
reached by waiting on `ConvergedTimer`.
|
||||
|
||||
Instead of interacting with the `ConvergedUID` with these two methods, we can
|
||||
instead use the `StartTimer` and `ResetTimer` methods which accomplish the same
|
||||
thing, but provide a `select`-free interface for different coding situations.
|
||||
If you need this functionality, please contact one of the maintainers and ask
|
||||
about adding this feature and improving these docs right here.
|
||||
|
||||
This particular facility is most likely not required for most resources. It may
|
||||
prove to be useful if a resource wants to start off a long operation, but avoid
|
||||
@@ -297,39 +353,32 @@ func (obj *FooRes) Watch() error {
|
||||
if err, obj.foo = OpenFoo(); err != nil {
|
||||
return err // we couldn't startup
|
||||
}
|
||||
defer obj.whatever.CloseFoo() // shutdown our
|
||||
defer obj.whatever.CloseFoo() // shutdown our Foo
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
|
||||
// the actual events!
|
||||
case event := <-obj.foo.Events:
|
||||
if is_an_event {
|
||||
send = true // used below
|
||||
obj.StateOK(false) // dirty
|
||||
send = true
|
||||
}
|
||||
|
||||
// event errors
|
||||
case err := <-obj.foo.Errors:
|
||||
return err // will cause a retry or permanent failure
|
||||
|
||||
case <-obj.init.Done: // signal for shutdown request
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event() // send the event!
|
||||
obj.init.Event()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,87 +386,246 @@ func (obj *FooRes) Watch() error {
|
||||
|
||||
#### Summary
|
||||
|
||||
* Remember to call the appropriate `converger` methods throughout the resource.
|
||||
* Remember to call `Startup` when the `Watch` is running successfully.
|
||||
* Remember to call `Running` when the `Watch` is running successfully.
|
||||
* Remember to process internal events and shutdown promptly if asked to.
|
||||
* Ensure the design of your resource is well thought out.
|
||||
* Have a look at the existing resources for a rough idea of how this all works.
|
||||
|
||||
### Compare
|
||||
### Cmp
|
||||
|
||||
```golang
|
||||
Compare(Res) bool
|
||||
Cmp(engine.Res) error
|
||||
```
|
||||
|
||||
Each resource must have a `Compare` method. This takes as input another resource
|
||||
and must return whether they are identical or not. This is used for identifying
|
||||
if an existing resource can be used in place of a new one with a similar set of
|
||||
parameters. In particular, when switching from one graph to a new (possibly
|
||||
identical) graph, this avoids recomputing the state for resources which don't
|
||||
change or that are sufficiently similar that they don't need to be swapped out.
|
||||
Each resource must have a `Cmp` method. It is an abbreviation for `Compare`. It
|
||||
takes as input another resource and must return whether they are identical or
|
||||
not. This is used for identifying if an existing resource can be used in place
|
||||
of a new one with a similar set of parameters. In particular, when switching
|
||||
from one graph to a new (possibly identical) graph, this avoids recomputing the
|
||||
state for resources which don't change or that are sufficiently similar that
|
||||
they don't need to be swapped out.
|
||||
|
||||
In general if all the resource properties are identical, then they usually don't
|
||||
need to be changed. On occasion, not all of them need to be compared, in
|
||||
particular if they store some generated state, or if they aren't significant in
|
||||
some way.
|
||||
|
||||
If the resource is identical, then you should return `nil`. If it is not, then
|
||||
you should return a short error message which gives the reason it differs.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *FooRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns if they are equivalent.
|
||||
func (obj *FooRes) Cmp(r engine.Res) error {
|
||||
// we can only compare FooRes to others of the same resource kind
|
||||
res, ok := r.(*FooRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.whatever != res.whatever {
|
||||
return false
|
||||
if obj.Whatever != res.Whatever {
|
||||
return fmt.Errorf("the Whatever param differs")
|
||||
}
|
||||
if obj.Flag != res.Flag {
|
||||
return false
|
||||
return fmt.Errorf("the Flag param differs")
|
||||
}
|
||||
|
||||
return true // they must match!
|
||||
return nil // they must match!
|
||||
}
|
||||
```
|
||||
|
||||
### UIDs
|
||||
## Traits
|
||||
|
||||
Resources can have different `traits`, which means they can be extended to have
|
||||
additional functionality or special properties. Those special properties are
|
||||
usually added by extending your resource so that it is compatible with
|
||||
additional interface that contain the `Res` interface. Each of these interfaces
|
||||
represents the additional functionality. Since in most cases this requires some
|
||||
common boilerplate, you can usually get some or most of the functionality by
|
||||
embedding the correct trait struct anonymously in your struct. This is shown in
|
||||
the struct example above. You'll always want to include the `Base` trait in all
|
||||
resources. This provides some basics which you'll always need.
|
||||
|
||||
What follows are a list of available traits.
|
||||
|
||||
### Refreshable
|
||||
|
||||
Some resources may choose to support receiving refresh notifications. In general
|
||||
these should be avoided if possible, but nevertheless, they do make sense in
|
||||
certain situations. Resources that support these need to verify if one was sent
|
||||
during the CheckApply phase of execution. This is accomplished by calling the
|
||||
`obj.init.Refresh() bool` method, and inspecting the return value. This is only
|
||||
necessary if you plan to perform a refresh action. Refresh actions should still
|
||||
respect the `apply` variable, and no system changes should be made if it is
|
||||
`false`. Refresh notifications are generated by any resource when an action is
|
||||
applied by that resource and are transmitted through graph edges which have
|
||||
enabled their propagation. Resources that currently perform some refresh action
|
||||
include `svc`, `timer`, and `password`.
|
||||
|
||||
It is very important that you include the `traits.Refreshable` struct in your
|
||||
resource. If you do not include this, then calling `obj.init.Refresh` may
|
||||
trigger a panic. This is programmer error.
|
||||
|
||||
### Edgeable
|
||||
|
||||
Edgeable is a trait that allows your resource to automatically connect itself to
|
||||
other resources that use this trait to add edge dependencies between the two. An
|
||||
older blog post on this topic is
|
||||
[available](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
|
||||
|
||||
After you've included this trait, you'll need to implement two methods on your
|
||||
resource.
|
||||
|
||||
#### UIDs
|
||||
|
||||
```golang
|
||||
UIDs() []ResUID
|
||||
UIDs() []engine.ResUID
|
||||
```
|
||||
|
||||
The `UIDs` method returns a list of `ResUID` interfaces that represent the
|
||||
particular resource uniquely. This is used with the AutoEdges API to determine
|
||||
if another resource can match a dependency to this one.
|
||||
|
||||
### AutoEdges
|
||||
#### AutoEdges
|
||||
|
||||
```golang
|
||||
AutoEdges() (AutoEdge, error)
|
||||
AutoEdges() (engine.AutoEdge, error)
|
||||
```
|
||||
|
||||
This returns a struct that implements the `AutoEdge` interface. This struct
|
||||
is used to match other resources that might be relevant dependencies for this
|
||||
resource.
|
||||
|
||||
### CollectPattern
|
||||
### Groupable
|
||||
|
||||
```golang
|
||||
CollectPattern() string
|
||||
```
|
||||
Groupable is a trait that can allow your resource automatically group itself to
|
||||
other resources. Doing so can reduce the resource or runtime burden on the
|
||||
engine, and improve performance in some scenarios. An older blog post on this
|
||||
topic is
|
||||
[available](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
|
||||
|
||||
### Sendable
|
||||
|
||||
Sendable is a trait that allows your resource to send values through the graph
|
||||
edges to another resource. These values are produced during `CheckApply`. They
|
||||
can be sent to any resource that has an appropriate parameter and that has the
|
||||
`Recvable` trait. You can read more about this in the Send/Recv section below.
|
||||
|
||||
### Recvable
|
||||
|
||||
Recvable is a trait that allows your resource to receive values through the
|
||||
graph edges from another resource. These values are consumed during the
|
||||
`CheckApply` phase, and can be detected there as well. They can be received from
|
||||
any resource that has an appropriate value and that has the `Sendable` trait.
|
||||
You can read more about this in the Send/Recv section below.
|
||||
|
||||
### Collectable
|
||||
|
||||
This is currently a stub and will be updated once the DSL is further along.
|
||||
|
||||
### UnmarshalYAML
|
||||
## Resource Initialization
|
||||
|
||||
During the resource initialization in `Init`, the engine will pass in a struct
|
||||
containing a bunch of data and methods. What follows is a description of each
|
||||
one and how it is used.
|
||||
|
||||
### Program
|
||||
|
||||
Program is a string containing the name of the program. Very few resources need
|
||||
this.
|
||||
|
||||
### Hostname
|
||||
|
||||
Hostname is the uuid for the host. It will be occasionally useful in some
|
||||
resources. It is preferable if you can avoid depending on this. It is possible
|
||||
that in the future this will be a channel which changes if the local hostname
|
||||
changes.
|
||||
|
||||
### Running
|
||||
|
||||
Running must be called after your watches are all started and ready. It is only
|
||||
called from within `Watch`. It is used to notify the engine that you're now
|
||||
ready to detect changes.
|
||||
|
||||
### Event
|
||||
|
||||
Event sends an event notifying the engine of a possible state change. It is
|
||||
only called from within `Watch`.
|
||||
|
||||
### Done
|
||||
|
||||
Done is a channel that closes when the engine wants us to shutdown. It is only
|
||||
called from within `Watch`.
|
||||
|
||||
### Refresh
|
||||
|
||||
Refresh returns whether the resource received a notification. This flag can be
|
||||
used to tell a `svc` to reload, or to perform some state change that wouldn't
|
||||
otherwise be noticed by inspection alone. You must implement the `Refreshable`
|
||||
trait for this to work. It is only called from within `CheckApply`.
|
||||
|
||||
### Send
|
||||
|
||||
Send exposes some variables you wish to send via the `Send/Recv` mechanism. You
|
||||
must implement the `Sendable` trait for this to work. It is only called from
|
||||
within `CheckApply`.
|
||||
|
||||
### Recv
|
||||
|
||||
Recv provides a map of variables which were sent to this resource via the
|
||||
`Send/Recv` mechanism. You must implement the `Recvable` trait for this to work.
|
||||
It is only called from within `CheckApply`.
|
||||
|
||||
### World
|
||||
|
||||
World provides a connection to the outside world. This is most often used for
|
||||
communicating with the distributed database. It can be used in `Init`,
|
||||
`CheckApply` and `Watch`. Use with discretion and understanding of the internals
|
||||
if needed in `Close`.
|
||||
|
||||
### VarDir
|
||||
|
||||
VarDir is a facility for local storage. It is used to return a path to a
|
||||
directory which may be used for temporary storage. It should be cleaned up on
|
||||
resource `Close` if the resource would like to delete the contents. The resource
|
||||
should not assume that the initial directory is empty, and it should be cleaned
|
||||
on `Init` if that is a requirement.
|
||||
|
||||
### Debug
|
||||
|
||||
Debug signals whether we are running in debugging mode. In this case, we might
|
||||
want to log additional messages.
|
||||
|
||||
### Logf
|
||||
|
||||
Logf is a logging facility which will correctly namespace any messages which you
|
||||
wish to pass on. You should use this instead of the log package directly for
|
||||
production quality resources.
|
||||
|
||||
## Further considerations
|
||||
|
||||
There is some additional information that any resource writer will need to know.
|
||||
Each issue is listed separately below!
|
||||
|
||||
### Resource registration
|
||||
|
||||
All resources must be registered with the engine so that they can be found. This
|
||||
also ensures they can be encoded and decoded. Make sure to include the following
|
||||
code snippet for this to work.
|
||||
|
||||
```golang
|
||||
func init() { // special golang method that runs once
|
||||
// set your resource kind and struct here (the kind must be lower case)
|
||||
engine.RegisterResource("foo", func() engine.Res { return &FooRes{} })
|
||||
}
|
||||
```
|
||||
|
||||
### YAML Unmarshalling
|
||||
|
||||
To support YAML unmarshalling for your resource, you must implement an
|
||||
additional method. It is recommended if you want to use your resource with the
|
||||
`Puppet` compiler.
|
||||
|
||||
```golang
|
||||
UnmarshalYAML(unmarshal func(interface{}) error) error // optional
|
||||
@@ -434,8 +642,8 @@ The signature intentionally matches what is required to satisfy the `go-yaml`
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes FooRes // indirection to avoid infinite recursion
|
||||
|
||||
@@ -455,105 +663,34 @@ func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
}
|
||||
```
|
||||
|
||||
## Further considerations
|
||||
|
||||
There is some additional information that any resource writer will need to know.
|
||||
Each issue is listed separately below!
|
||||
|
||||
### Resource struct
|
||||
|
||||
Each resource will implement methods as pointer receivers on a resource struct.
|
||||
The resource struct must include an anonymous reference to the `BaseRes` struct.
|
||||
The naming convention for resources is that they end with a `Res` suffix. If
|
||||
you'd like your resource to be accessible by the `YAML` graph API (GAPI), then
|
||||
you'll need to include the appropriate YAML fields as shown below.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
type FooRes struct {
|
||||
BaseRes `yaml:",inline"` // base properties
|
||||
|
||||
Whatever string `yaml:"whatever"` // you pick!
|
||||
Bar int // no yaml, used as public output value for send/recv
|
||||
Baz bool `yaml:"baz"` // something else
|
||||
|
||||
something string // some private field
|
||||
}
|
||||
```
|
||||
|
||||
### Resource registration
|
||||
|
||||
All resources must be registered with the engine so that they can be found. This
|
||||
also ensures they can be encoded and decoded. Make sure to include the following
|
||||
code snippet for this to work.
|
||||
|
||||
```golang
|
||||
func init() { // special golang method that runs once
|
||||
// set your resource kind and struct here (the kind must be lower case)
|
||||
RegisterResource("foo", func() Res { return &FooRes{} })
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic edges
|
||||
|
||||
Automatic edges in `mgmt` are well described in [this article](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
|
||||
The best example of this technique can be seen in the `svc` resource.
|
||||
Unfortunately no further documentation about this subject has been written. To
|
||||
expand this section, please send a patch! Please contact us if you'd like to
|
||||
work on a resource that uses this feature, or to add it to an existing one!
|
||||
|
||||
## Automatic grouping
|
||||
|
||||
Automatic grouping in `mgmt` is well described in [this article](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
|
||||
The best example of this technique can be seen in the `pkg` resource.
|
||||
Unfortunately no further documentation about this subject has been written. To
|
||||
expand this section, please send a patch! Please contact us if you'd like to
|
||||
work on a resource that uses this feature, or to add it to an existing one!
|
||||
|
||||
## Send/Recv
|
||||
|
||||
In `mgmt` there is a novel concept called _Send/Recv_. For some background,
|
||||
please [read the introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/).
|
||||
please read the [introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/).
|
||||
When using this feature, the engine will automatically send the user specified
|
||||
value to the intended destination without requiring any resource specific code.
|
||||
value to the intended destination without requiring much resource specific code.
|
||||
Any time that one of the destination values is changed, the engine automatically
|
||||
marks the resource state as `dirty`. To detect if a particular value was
|
||||
received, and if it changed (during this invocation of CheckApply) from the
|
||||
previous value, you can query the Recv parameter. It will contain a `map` of all
|
||||
the keys which can be received on, and the value has a `Changed` property which
|
||||
will indicate whether the value was updated on this particular `CheckApply`
|
||||
invocation. The type of the sending key must match that of the receiving one.
|
||||
This can _only_ be done inside of the `CheckApply` function!
|
||||
received, and if it changed (during this invocation of `CheckApply`) from the
|
||||
previous value, you can query the `obj.init.Recv()` method. It will contain a
|
||||
`map` of all the keys which can be received on, and the value has a `Changed`
|
||||
property which will indicate whether the value was updated on this particular
|
||||
`CheckApply` invocation. The type of the sending key must match that of the
|
||||
receiving one. This can _only_ be done inside of the `CheckApply` function!
|
||||
|
||||
```golang
|
||||
// inside CheckApply, probably near the top
|
||||
if val, exists := obj.Recv["SomeKey"]; exists {
|
||||
log.Printf("SomeKey was sent to us from: %s.%s", val.Res, val.Key)
|
||||
if val, exists := obj.init.Recv()["SomeKey"]; exists {
|
||||
obj.init.Logf("the SomeKey param was sent to us from: %s.%s", val.Res, val.Key)
|
||||
if val.Changed {
|
||||
log.Printf("SomeKey was just updated!")
|
||||
obj.init.Logf("the SomeKey param was just updated!")
|
||||
// you may want to invalidate some local cache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Astute readers will note that there isn't anything that prevents a user from
|
||||
sending an identically typed value to some arbitrary (public) key that the
|
||||
resource author hadn't considered! While this is true, resources should probably
|
||||
work within this problem space anyways. The rule of thumb is that any public
|
||||
parameter which is normally used in a resource can be used safely.
|
||||
|
||||
One subtle scenario is that if a resource creates a local cache or stores a
|
||||
computation that depends on the value of a public parameter and will require
|
||||
invalidation should that public parameter change, then you must detect that
|
||||
scenario and invalidate the cache when it occurs. This *must* be processed
|
||||
before there is a possibility of failure in CheckApply, because if we fail (and
|
||||
possibly run again) the subsequent send->recv transfer might not have a new
|
||||
value to copy, and therefore we won't see this notification of change.
|
||||
Therefore, it is important to process these promptly, if they must not be lost,
|
||||
such as for cache invalidation.
|
||||
|
||||
Remember, `Send/Recv` only changes your resource code if you cache state.
|
||||
The specifics of resource sending are not currently documented. Please send a
|
||||
patch here!
|
||||
|
||||
## Composite resources
|
||||
|
||||
@@ -619,11 +756,37 @@ Feel free to use this pattern if you're convinced it's necessary. Alternatively,
|
||||
if you think I got the `Res` API wrong and you have an improvement, please let
|
||||
us know!
|
||||
|
||||
### Why do resources have both a `Cmp` method and an `IFF` (on the UID) method?
|
||||
|
||||
The `Cmp()` methods are for determining if two resources are effectively the
|
||||
same, which is used to make graph change delta's efficient. This is when we want
|
||||
to change from the current running graph to a new graph, but preserve the common
|
||||
vertices. Since we want to make this process efficient, we only update the parts
|
||||
that are different, and leave everything else alone. This `Cmp()` method can
|
||||
tell us if two resources are the same. In case it is not obvious, `cmp` is an
|
||||
abbrev. for compare.
|
||||
|
||||
The `IFF()` method is part of the whole UID system, which is for discerning if a
|
||||
resource meets the requirements another expects for an automatic edge. This is
|
||||
because the automatic edge system assumes a unified UID pattern to test for
|
||||
equality. In the future it might be helpful or sane to merge the two similar
|
||||
comparison functions although for now they are separate because they are
|
||||
actually answer different questions.
|
||||
|
||||
### What new resource primitives need writing?
|
||||
|
||||
There are still many ideas for new resources that haven't been written yet. If
|
||||
you'd like to contribute one, please contact us and tell us about your idea!
|
||||
|
||||
### Is the resource API stable? Does it ever change?
|
||||
|
||||
Since we are pre 1.0, the resource API is not guaranteed to be stable, however
|
||||
it is not expected to change significantly. The last major change kept the
|
||||
core functionality nearly identical, simplified the implementation of all the
|
||||
resources, and took about five to ten minutes to port each resource to the new
|
||||
API. The fundamental logic and behaviour behind the resource API has not changed
|
||||
since it was initially introduced.
|
||||
|
||||
### Where can I find more information about mgmt?
|
||||
|
||||
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
||||
|
||||
@@ -13,21 +13,30 @@ separately. Certain meta parameters aren't very useful when combined with
|
||||
certain resources, but in general, it should be fairly obvious, such as when
|
||||
combining the `noop` meta parameter with the [Noop](#Noop) resource.
|
||||
|
||||
You might want to look at the [generated documentation](https://godoc.org/github.com/purpleidea/mgmt/resources)
|
||||
You might want to look at the [generated documentation](https://godoc.org/github.com/purpleidea/mgmt/engine/resources)
|
||||
for more up-to-date information about these resources.
|
||||
|
||||
* [Augeas](#Augeas): Manipulate files using augeas.
|
||||
* [Consul:KV](#ConsulKV): Set keys in a Consul datastore.
|
||||
* [Docker](#Docker):[Container](#Container) Manage docker containers.
|
||||
* [Exec](#Exec): Execute shell commands on the system.
|
||||
* [File](#File): Manage files and directories.
|
||||
* [Group](#Group): Manage system groups.
|
||||
* [Hostname](#Hostname): Manages the hostname on the system.
|
||||
* [KV](#KV): Set a key value pair in our shared world database.
|
||||
* [Msg](#Msg): Send log messages.
|
||||
* [Net](#Net): Manage a local network interface.
|
||||
* [Noop](#Noop): A simple resource that does nothing.
|
||||
* [Nspawn](#Nspawn): Manage systemd-machined nspawn containers.
|
||||
* [Password](#Password): Create random password strings.
|
||||
* [Pkg](#Pkg): Manage system packages with PackageKit.
|
||||
* [Print](#Print): Print messages to the console.
|
||||
* [Svc](#Svc): Manage system systemd services.
|
||||
* [Test](#Test): A mostly harmless resource that is used for internal testing.
|
||||
* [Tftp:File](#TftpFile): Add files to the small embedded embedded tftp server.
|
||||
* [Tftp:Server](#TftpServer): Run a small embedded tftp server.
|
||||
* [Timer](#Timer): Manage system systemd services.
|
||||
* [User](#User): Manage system users.
|
||||
* [Virt](#Virt): Manage virtual machines with libvirt.
|
||||
|
||||
## Augeas
|
||||
@@ -35,6 +44,22 @@ for more up-to-date information about these resources.
|
||||
The augeas resource uses [augeas](http://augeas.net/) commands to manipulate
|
||||
files.
|
||||
|
||||
## Docker
|
||||
|
||||
### Container
|
||||
|
||||
The docker:container resource manages docker containers.
|
||||
|
||||
It has the following properties:
|
||||
|
||||
* `state`: either `running`, `stopped`, or `removed`
|
||||
* `image`: docker `image` or `image:tag`
|
||||
* `cmd`: a command or list of commands to run on the container
|
||||
* `env`: a list of environment variables, e.g. `["VAR=val",],`
|
||||
* `ports`: a map of portmappings, e.g. `{"tcp" => {80 => 8080, 443 => 8443,},},`
|
||||
* `apiversion:` override the host's default docker version, e.g. `"v1.35"`
|
||||
* `force`: destroy and rebuild the container instead of erroring on wrong image
|
||||
|
||||
## Exec
|
||||
|
||||
The exec resource can execute commands on your system.
|
||||
@@ -46,10 +71,10 @@ identified by a trailing slash in their path name. File have no such slash.
|
||||
|
||||
It has the following properties:
|
||||
|
||||
* `path`: file path (directories have a trailing slash here)
|
||||
* `path`: absolute file path (directories have a trailing slash here)
|
||||
* `state`: either `exists`, `absent`, or undefined
|
||||
* `content`: raw file content
|
||||
* `state`: either `exists` (the default value) or `absent`
|
||||
* `mode`: octal unix file permissions
|
||||
* `mode`: octal unix file permissions or symbolic string
|
||||
* `owner`: username or uid for the file owner
|
||||
* `group`: group name or gid for the file group
|
||||
|
||||
@@ -57,6 +82,16 @@ It has the following properties:
|
||||
|
||||
The path property specifies the file or directory that we are managing.
|
||||
|
||||
### State
|
||||
|
||||
The state property describes the action we'd like to apply for the resource. The
|
||||
possible values are: `exists` and `absent`. If you do not specify either of
|
||||
these, it is undefined. Without specifying this value as `exists`, another param
|
||||
cannot cause a file to get implicitly created. When specifying this value as
|
||||
`absent`, you should not specify any other params that would normally change the
|
||||
file. For example, if you specify `content` and this param is `absent`, then you
|
||||
will get an engine validation error.
|
||||
|
||||
### Content
|
||||
|
||||
The content property is a string that specifies the desired file contents.
|
||||
@@ -66,10 +101,12 @@ The content property is a string that specifies the desired file contents.
|
||||
The source property points to a source file or directory path that we wish to
|
||||
copy over and use as the desired contents for our resource.
|
||||
|
||||
### State
|
||||
### Fragments
|
||||
|
||||
The state property describes the action we'd like to apply for the resource. The
|
||||
possible values are: `exists` and `absent`.
|
||||
The fragments property lets you specify a list of files to concatenate together
|
||||
to make up the contents of this file. They will be combined in the order that
|
||||
they are listed in. If one of the files specified is a directory, then the
|
||||
files in that top-level directory will be themselves combined together and used.
|
||||
|
||||
### Recurse
|
||||
|
||||
@@ -82,6 +119,16 @@ The force property is required if we want the file resource to be able to change
|
||||
a file into a directory or vice-versa. If such a change is needed, but the force
|
||||
property is not set to `true`, then this file resource will error.
|
||||
|
||||
### Purge
|
||||
|
||||
The purge property is used when this file represents a directory, and we'd like
|
||||
to remove any unmanaged files from within it. Please note that any unmanaged
|
||||
files in a directory with this flag set will be irreversibly deleted.
|
||||
|
||||
## Group
|
||||
|
||||
The group resource manages the system groups from `/etc/group`.
|
||||
|
||||
## Hostname
|
||||
|
||||
The hostname resource manages static, transient/dynamic and pretty hostnames
|
||||
@@ -143,6 +190,10 @@ would expect.
|
||||
The msg resource sends messages to the main log, or an external service such
|
||||
as systemd's journal.
|
||||
|
||||
## Net
|
||||
|
||||
The net resource manages a local network interface using netlink.
|
||||
|
||||
## Noop
|
||||
|
||||
The noop resource does absolutely nothing. It does have some utility in testing
|
||||
@@ -164,13 +215,35 @@ different distributions because it uses the underlying packagekit facility which
|
||||
supports different backends for different environments. This ensures that we
|
||||
have great Debian (deb/dpkg) and Fedora (rpm/dnf) support simultaneously.
|
||||
|
||||
## Print
|
||||
|
||||
The print resource prints messages to the console.
|
||||
|
||||
## Svc
|
||||
|
||||
The service resource is still very WIP. Please help us my improving it!
|
||||
The service resource is still very WIP. Please help us by improving it!
|
||||
|
||||
## Test
|
||||
|
||||
The test resource is mostly harmless and is used for internal tests.
|
||||
|
||||
## Tftp:File
|
||||
|
||||
This adds files to the running tftp server. It's useful because it allows you to
|
||||
add individual files without needing to create them on disk.
|
||||
|
||||
## Tftp:Server
|
||||
|
||||
Run a small embedded tftp server. This doesn't apply any state, but instead runs
|
||||
a pure golang tftp server in the Watch loop.
|
||||
|
||||
## Timer
|
||||
|
||||
This resource needs better documentation. Please help us my improving it!
|
||||
This resource needs better documentation. Please help us by improving it!
|
||||
|
||||
## User
|
||||
|
||||
The user resource manages the system users from `/etc/passwd`.
|
||||
|
||||
## Virt
|
||||
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
# Style guide
|
||||
|
||||
## Overview
|
||||
This document aims to be a reference for the desired style for patches to mgmt,
|
||||
and the associated `mcl` language. In particular it describes conventions which
|
||||
are not officially enforced by tools and in test cases, or that aren't clearly
|
||||
defined elsewhere. We try to turn as many of these into automated tests as we
|
||||
can. If something here is not defined in a test, or you think it should be,
|
||||
please write one! Even better, you can write a tool to automatically fix it,
|
||||
since this is more useful and can easily be turned into a test!
|
||||
|
||||
This document aims to be a reference for the desired style for patches to mgmt.
|
||||
In particular it describes conventions which we use which are not officially
|
||||
enforced by the `gofmt` tool, and which might not be clearly defined elsewhere.
|
||||
Most of these are common sense to seasoned programmers, and we hope this will be
|
||||
a useful reference for new programmers.
|
||||
## Overview for golang code
|
||||
|
||||
Most style issues are enforced by the `gofmt` tool. Other style aspects are
|
||||
often common sense to seasoned programmers, and we hope this will be a useful
|
||||
reference for new programmers.
|
||||
|
||||
There are a lot of useful code review comments described
|
||||
[here](https://github.com/golang/go/wiki/CodeReviewComments). We don't
|
||||
necessarily follow everything strictly, but it is in general a very good guide.
|
||||
|
||||
## Basics
|
||||
### Basics
|
||||
|
||||
* All of our golang code is formatted with `gofmt`.
|
||||
|
||||
## Comments
|
||||
### Comments
|
||||
|
||||
All of our code is commented with the minimums required for `godoc` to function,
|
||||
and so that our comments pass `golint`. Code comments should either be full
|
||||
@@ -28,7 +34,7 @@ They should explain algorithms, describe non-obvious behaviour, or situations
|
||||
which would otherwise need explanation or additional research during a code
|
||||
review. Notes about use of unfamiliar API's is a good idea for a code comment.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
Here you can see a function with the correct `godoc` string. The first word must
|
||||
match the name of the function. It is _not_ capitalized because the function is
|
||||
@@ -41,7 +47,7 @@ func square(x int) int {
|
||||
}
|
||||
```
|
||||
|
||||
## Line length
|
||||
### Line length
|
||||
|
||||
In general we try to stick to 80 character lines when it is appropriate. It is
|
||||
almost *always* appropriate for function `godoc` comments and most longer
|
||||
@@ -55,7 +61,13 @@ Occasionally inline, two line source code comments are used within a function.
|
||||
These should usually be balanced so that you don't have one line with 78
|
||||
characters and the second with only four. Split the comment between the two.
|
||||
|
||||
## Method receiver naming
|
||||
### Default values
|
||||
|
||||
Whenever a constant or function parameter is defined, try and have the safer or
|
||||
default value be the `zero` value. For example, instead of `const NoDanger`, use
|
||||
`const AllowDanger` so that the `false` value is the safe scenario.
|
||||
|
||||
### Method receiver naming
|
||||
|
||||
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)
|
||||
to the specialized naming of the method receiver variable, we usually name all
|
||||
@@ -65,7 +77,7 @@ makes the code easier to read since you don't need to remember the name of the
|
||||
method receiver variable in each different method. This is very similar to what
|
||||
is done in `python`.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Bar does a thing, and returns the number of baz results found in our
|
||||
@@ -78,7 +90,58 @@ func (obj *Foo) Bar(baz string) int {
|
||||
}
|
||||
```
|
||||
|
||||
## Consistent ordering
|
||||
### Variable naming
|
||||
|
||||
We prefer shorter, scoped variables rather than `unnecessarilyLongIdentifiers`.
|
||||
Remember the scoping rules and feel free to use new variables where appropriate.
|
||||
For example, in a short string snippet you can use `s` instead of `myString`, as
|
||||
well as other common choices. `i` is a common `int` counter, `f` for files, `fn`
|
||||
for functions, `x` for something else and so on.
|
||||
|
||||
### Variable re-use
|
||||
|
||||
Feel free to create and use new variables instead of attempting to re-use the
|
||||
same string. For example, if a function input arg is named `s`, you can use a
|
||||
new variable to receive the first computation result on `s` instead of storing
|
||||
it back into the original `s`. This avoids confusion if a different part of the
|
||||
code wants to read the original input, and it avoids any chance of edit by
|
||||
reference of the original callers copy of the variable.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
MyNotIdealFunc(s string, b bool) string {
|
||||
if !b {
|
||||
return s + "hey"
|
||||
}
|
||||
s = strings.Replace(s, "blah", "", -1) // not ideal (re-use of `s` var)
|
||||
return s
|
||||
}
|
||||
|
||||
MyOkayFunc(s string, b bool) string {
|
||||
if !b {
|
||||
return s + "hey"
|
||||
}
|
||||
s2 := strings.Replace(s, "blah", "", -1) // doesn't re-use `s` variable
|
||||
return s2
|
||||
}
|
||||
|
||||
MyGreatFunc(s string, b bool) string {
|
||||
if !b {
|
||||
return s + "hey"
|
||||
}
|
||||
return strings.Replace(s, "blah", "", -1) // even cleaner
|
||||
}
|
||||
```
|
||||
|
||||
### Constants in code
|
||||
|
||||
If a function takes a specifier (often a bool) it's sometimes better to name
|
||||
that variable (often with a `const`) rather than leaving a naked `bool` in the
|
||||
code. For example, `x := MyFoo("blah", false)` is less clear than
|
||||
`const useMagic = false; x := MyFoo("blah", useMagic)`.
|
||||
|
||||
### Consistent ordering
|
||||
|
||||
In general we try to preserve a logical ordering in source files which usually
|
||||
matches the common order of execution that a _lazy evaluator_ would follow.
|
||||
@@ -90,6 +153,72 @@ declared in the interface.
|
||||
When implementing code for the various types in the language, please follow this
|
||||
order: `bool`, `str`, `int`, `float`, `list`, `map`, `struct`, `func`.
|
||||
|
||||
For other aspects where you have a set of items, try to be internally consistent
|
||||
as well. For example, if you have two switch statements with `A`, `B`, and `C`,
|
||||
please use the same ordering for these elements elsewhere that they appear in
|
||||
the code and in the commentary if it is not illogical to do so.
|
||||
|
||||
### Product identifiers
|
||||
|
||||
Try to avoid references in the code to `mgmt` or a specific program name string
|
||||
if possible. This makes it easier to rename code if we ever pick a better name
|
||||
or support `libmgmt` better if we embed it. You can use the `Program` variable
|
||||
which is available in numerous places if you want a string to put in the logs.
|
||||
|
||||
It is also recommended to avoid the `go` (programming language name) string if
|
||||
possible. Try to use `golang` if required, since the word `go` is already
|
||||
overloaded, and in particular it was even already used by the
|
||||
[`go!`](https://en.wikipedia.org/wiki/Go!_(programming_language)).
|
||||
|
||||
## Overview for mcl code
|
||||
|
||||
The `mcl` language is quite new, so this guide will probably change over time as
|
||||
we find what's best, and hopefully we'll be able to add an `mclfmt` tool in the
|
||||
future so that less of this needs to be documented. (Patches welcome!)
|
||||
|
||||
### Indentation
|
||||
|
||||
Code indentation is done with tabs. The tab-width is a private preference, which
|
||||
is the beauty of using tabs: you can have your own personal preference. The
|
||||
inventor of `mgmt` uses and recommends a width of eight, and that is what should
|
||||
be used if your tool requires a modeline to be publicly committed.
|
||||
|
||||
### Line length
|
||||
|
||||
We recommend you stick to 80 char line width. If you find yourself with deeper
|
||||
nesting, it might be a hint that your code could be refactored in a more
|
||||
pleasant way.
|
||||
|
||||
### Capitalization
|
||||
|
||||
At the moment, variables, function names, and classes are all lowercase and do
|
||||
not contain underscores. We will probably figure out what style to recommend
|
||||
when the language is a bit further along. For example, we haven't decided if we
|
||||
should have a notion of public and private variables, and if we'd like to
|
||||
reserve capitalization for this situation.
|
||||
|
||||
### Module naming
|
||||
|
||||
We recommend you name your modules with an `mgmt-` prefix. For example, a module
|
||||
about bananas might be named `mgmt-banana`. This is helpful for the useful magic
|
||||
built-in to the module import code, which will by default take a remote import
|
||||
like: `import "https://github.com/purpleidea/mgmt-banana/"` and namespace it as
|
||||
`banana`. Of course you can always pick the namespace yourself on import with:
|
||||
`import "https://github.com/purpleidea/mgmt-banana/" as tomato` or something
|
||||
similar.
|
||||
|
||||
### Licensing
|
||||
|
||||
We believe that sharing code helps reduce unnecessary re-invention, so that we
|
||||
can [stand on the shoulders of giants](https://en.wikipedia.org/wiki/Standing_on_the_shoulders_of_giants)
|
||||
and hopefully make faster progress in science, medicine, exploration, etc... As
|
||||
a result, we recommend releasing your modules under the [LGPLv3+](https://www.gnu.org/licenses/lgpl-3.0.en.html)
|
||||
license for the maximum balance of freedom and re-usability. We strongly oppose
|
||||
any [CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement)
|
||||
requirements and believe that the ["inbound==outbound"](https://ref.fedorapeople.org/fontana-linuxcon.html#slide2)
|
||||
rule applies. Lastly, we do not support software patents and we hope you don't
|
||||
either!
|
||||
|
||||
## Suggestions
|
||||
|
||||
If you have any ideas for suggestions or other improvements to this guide,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,18 +15,66 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ResUID is a unique identifier for a resource, namely it's name, and the kind ("type").
|
||||
// EdgeableRes is the interface a resource must implement to support automatic
|
||||
// edges. Both the vertices involved in an edge need to implement this for it to
|
||||
// be able to work.
|
||||
type EdgeableRes interface {
|
||||
Res // implement everything in Res but add the additional requirements
|
||||
|
||||
// AutoEdgeMeta lets you get or set meta params for the automatic edges
|
||||
// trait.
|
||||
AutoEdgeMeta() *AutoEdgeMeta
|
||||
|
||||
// SetAutoEdgeMeta lets you set all of the meta params for the automatic
|
||||
// edges trait in a single call.
|
||||
SetAutoEdgeMeta(*AutoEdgeMeta)
|
||||
|
||||
// UIDs includes all params to make a unique identification of this
|
||||
// object.
|
||||
UIDs() []ResUID // most resources only return one
|
||||
|
||||
// AutoEdges returns a struct that implements the AutoEdge interface.
|
||||
// This interface can be used to generate automatic edges to other
|
||||
// resources.
|
||||
AutoEdges() (AutoEdge, error)
|
||||
}
|
||||
|
||||
// AutoEdgeMeta provides some parameters specific to automatic edges.
|
||||
// TODO: currently this only supports disabling the feature per-resource, but in
|
||||
// the future you could conceivably have some small pattern to control it better
|
||||
type AutoEdgeMeta struct {
|
||||
// Disabled specifies that automatic edges should be disabled for this
|
||||
// resource.
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// Cmp compares two AutoEdgeMeta structs and determines if they're equivalent.
|
||||
func (obj *AutoEdgeMeta) Cmp(aem *AutoEdgeMeta) error {
|
||||
if obj.Disabled != aem.Disabled {
|
||||
return fmt.Errorf("values for Disabled are different")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// The AutoEdge interface is used to implement the autoedges feature.
|
||||
type AutoEdge interface {
|
||||
Next() []ResUID // call to get list of edges to add
|
||||
Test([]bool) bool // call until false
|
||||
}
|
||||
|
||||
// ResUID is a unique identifier for a resource, namely it's name, and the kind
|
||||
// ("type").
|
||||
type ResUID interface {
|
||||
fmt.Stringer // String() string
|
||||
|
||||
GetName() string
|
||||
GetKind() string
|
||||
fmt.Stringer // String() string
|
||||
|
||||
IFF(ResUID) bool
|
||||
|
||||
@@ -57,9 +105,9 @@ func (obj *BaseUID) String() string {
|
||||
}
|
||||
|
||||
// IFF looks at two UID's and if and only if they are equivalent, returns true.
|
||||
// If they are not equivalent, it returns false.
|
||||
// Most resources will want to override this method, since it does the important
|
||||
// work of actually discerning if two resources are identical in function.
|
||||
// If they are not equivalent, it returns false. Most resources will want to
|
||||
// override this method, since it does the important work of actually discerning
|
||||
// if two resources are identical in function.
|
||||
func (obj *BaseUID) IFF(uid ResUID) bool {
|
||||
res, ok := uid.(*BaseUID)
|
||||
if !ok {
|
||||
@@ -72,7 +120,7 @@ func (obj *BaseUID) IFF(uid ResUID) bool {
|
||||
// happens before the generator.
|
||||
func (obj *BaseUID) IsReversed() bool {
|
||||
if obj.Reversed == nil {
|
||||
log.Fatal("Programming error!")
|
||||
panic("programming error!")
|
||||
}
|
||||
return *obj.Reversed
|
||||
}
|
||||
38
engine/autoedge_test.go
Normal file
38
engine/autoedge_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIFF1(t *testing.T) {
|
||||
uid := &BaseUID{Name: "/tmp/unit-test"}
|
||||
same := &BaseUID{Name: "/tmp/unit-test"}
|
||||
diff := &BaseUID{Name: "/tmp/other-file"}
|
||||
|
||||
if !uid.IFF(same) {
|
||||
t.Errorf("basic resource UIDs with the same name should satisfy each other's IFF condition")
|
||||
}
|
||||
|
||||
if uid.IFF(diff) {
|
||||
t.Errorf("basic resource UIDs with different names should NOT satisfy each other's IFF condition")
|
||||
}
|
||||
}
|
||||
88
engine/autogroup.go
Normal file
88
engine/autogroup.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
// GroupableRes is the interface a resource must implement to support automatic
|
||||
// grouping. Default implementations for most of the methods declared in this
|
||||
// interface can be obtained for your resource by anonymously adding the
|
||||
// traits.Groupable struct to your resource implementation.
|
||||
type GroupableRes interface {
|
||||
Res // implement everything in Res but add the additional requirements
|
||||
|
||||
// AutoGroupMeta lets you get or set meta params for the automatic
|
||||
// grouping trait.
|
||||
AutoGroupMeta() *AutoGroupMeta
|
||||
|
||||
// SetAutoGroupMeta lets you set all of the meta params for the
|
||||
// automatic grouping trait in a single call.
|
||||
SetAutoGroupMeta(*AutoGroupMeta)
|
||||
|
||||
// GroupCmp compares two resources and decides if they're suitable for
|
||||
// grouping. This usually needs to be unique to your resource.
|
||||
GroupCmp(res GroupableRes) error
|
||||
|
||||
// GroupRes groups resource argument (res) into self.
|
||||
GroupRes(res GroupableRes) error
|
||||
|
||||
// IsGrouped determines if we are grouped.
|
||||
IsGrouped() bool // am I grouped?
|
||||
|
||||
// SetGrouped sets a flag to tell if we are grouped.
|
||||
SetGrouped(bool)
|
||||
|
||||
// GetGroup returns everyone grouped inside me.
|
||||
GetGroup() []GroupableRes // return everyone grouped inside me
|
||||
|
||||
// SetGroup sets the grouped resources into me.
|
||||
SetGroup([]GroupableRes)
|
||||
}
|
||||
|
||||
// AutoGroupMeta provides some parameters specific to automatic grouping.
|
||||
// TODO: currently this only supports disabling the feature per-resource, but in
|
||||
// the future you could conceivably have some small pattern to control it better
|
||||
type AutoGroupMeta struct {
|
||||
// Disabled specifies that automatic grouping should be disabled for
|
||||
// this resource.
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
// Cmp compares two AutoGroupMeta structs and determines if they're equivalent.
|
||||
func (obj *AutoGroupMeta) Cmp(agm *AutoGroupMeta) error {
|
||||
if obj.Disabled != agm.Disabled {
|
||||
return fmt.Errorf("values for Disabled are different")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AutoGrouper is the required interface to implement an autogrouping algorithm.
|
||||
type AutoGrouper interface {
|
||||
// listed in the order these are typically called in...
|
||||
Name() string // friendly identifier
|
||||
Init(*pgraph.Graph) error // only call once
|
||||
VertexNext() (pgraph.Vertex, pgraph.Vertex, error) // mostly algorithmic
|
||||
VertexCmp(pgraph.Vertex, pgraph.Vertex) error // can we merge these ?
|
||||
VertexMerge(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error) // vertex merge fn to use
|
||||
EdgeMerge(pgraph.Edge, pgraph.Edge) pgraph.Edge // edge merge fn to use
|
||||
VertexTest(bool) (bool, error) // call until false
|
||||
}
|
||||
343
engine/cmp.go
Normal file
343
engine/cmp.go
Normal file
@@ -0,0 +1,343 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
// ResCmp compares two resources by checking multiple aspects. This is the main
|
||||
// entry point for running all the compare steps on two resources. This code is
|
||||
// very similar to AdaptCmp.
|
||||
func ResCmp(r1, r2 Res) error {
|
||||
if r1.Kind() != r2.Kind() {
|
||||
return fmt.Errorf("kind differs")
|
||||
}
|
||||
if r1.Name() != r2.Name() {
|
||||
return fmt.Errorf("name differs")
|
||||
}
|
||||
|
||||
if err := r1.Cmp(r2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: do we need to compare other traits/metaparams?
|
||||
|
||||
m1 := r1.MetaParams()
|
||||
m2 := r2.MetaParams()
|
||||
if (m1 == nil) != (m2 == nil) { // xor
|
||||
return fmt.Errorf("meta params differ")
|
||||
}
|
||||
if m1 != nil && m2 != nil {
|
||||
if err := m1.Cmp(m2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r1x, ok1 := r1.(RefreshableRes)
|
||||
r2x, ok2 := r2.(RefreshableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("refreshable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1x.Refresh() != r2x.Refresh() {
|
||||
return fmt.Errorf("refresh differs")
|
||||
}
|
||||
}
|
||||
|
||||
// compare meta params for resources with auto edges
|
||||
r1e, ok1 := r1.(EdgeableRes)
|
||||
r2e, ok2 := r2.(EdgeableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("edgeable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1e.AutoEdgeMeta().Cmp(r2e.AutoEdgeMeta()) != nil {
|
||||
return fmt.Errorf("autoedge differs")
|
||||
}
|
||||
}
|
||||
|
||||
// compare meta params for resources with auto grouping
|
||||
r1g, ok1 := r1.(GroupableRes)
|
||||
r2g, ok2 := r2.(GroupableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("groupable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1g.AutoGroupMeta().Cmp(r2g.AutoGroupMeta()) != nil {
|
||||
return fmt.Errorf("autogroup differs")
|
||||
}
|
||||
|
||||
// if resources are grouped, are the groups the same?
|
||||
if i, j := r1g.GetGroup(), r2g.GetGroup(); len(i) != len(j) {
|
||||
return fmt.Errorf("autogroup groups differ")
|
||||
} else if len(i) > 0 { // trick the golinter
|
||||
|
||||
// Sort works with Res, so convert the lists to that
|
||||
iRes := []Res{}
|
||||
for _, r := range i {
|
||||
res := r.(Res)
|
||||
iRes = append(iRes, res)
|
||||
}
|
||||
jRes := []Res{}
|
||||
for _, r := range j {
|
||||
res := r.(Res)
|
||||
jRes = append(jRes, res)
|
||||
}
|
||||
|
||||
ix, jx := Sort(iRes), Sort(jRes) // now sort :)
|
||||
for k := range ix {
|
||||
// compare sub resources
|
||||
if err := ResCmp(ix[k], jx[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r1r, ok1 := r1.(RecvableRes)
|
||||
r2r, ok2 := r2.(RecvableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("recvable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
v1 := r1r.Recv()
|
||||
v2 := r2r.Recv()
|
||||
|
||||
if (v1 == nil) != (v2 == nil) { // xor
|
||||
return fmt.Errorf("recv params differ")
|
||||
}
|
||||
if v1 != nil && v2 != nil {
|
||||
// TODO: until we hit this code path, don't allow
|
||||
// comparing anything that has this set to non-zero
|
||||
if len(v1) != 0 || len(v2) != 0 {
|
||||
return fmt.Errorf("recv params exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r1s, ok1 := r1.(SendableRes)
|
||||
r2s, ok2 := r2.(SendableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("sendable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
s1 := r1s.Sent()
|
||||
s2 := r2s.Sent()
|
||||
|
||||
if (s1 == nil) != (s2 == nil) { // xor
|
||||
return fmt.Errorf("send params differ")
|
||||
}
|
||||
if s1 != nil && s2 != nil {
|
||||
// TODO: until we hit this code path, don't allow
|
||||
// adapting anything that has this set to non-nil
|
||||
return fmt.Errorf("send params exist")
|
||||
}
|
||||
}
|
||||
|
||||
// compare meta params for resources with reversible traits
|
||||
r1v, ok1 := r1.(ReversibleRes)
|
||||
r2v, ok2 := r2.(ReversibleRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("reversible differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
|
||||
return fmt.Errorf("reversible differs")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdaptCmp compares two resources by checking multiple aspects. This is the
|
||||
// main entry point for running all the compatible compare steps on two
|
||||
// resources. This code is very similar to ResCmp.
|
||||
func AdaptCmp(r1, r2 CompatibleRes) error {
|
||||
if r1.Kind() != r2.Kind() {
|
||||
return fmt.Errorf("kind differs")
|
||||
}
|
||||
if r1.Name() != r2.Name() {
|
||||
return fmt.Errorf("name differs")
|
||||
}
|
||||
|
||||
// run `Adapts` instead of `Cmp`
|
||||
if err := r1.Adapts(r2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: do we need to compare other traits/metaparams?
|
||||
|
||||
m1 := r1.MetaParams()
|
||||
m2 := r2.MetaParams()
|
||||
if (m1 == nil) != (m2 == nil) { // xor
|
||||
return fmt.Errorf("meta params differ")
|
||||
}
|
||||
if m1 != nil && m2 != nil {
|
||||
if err := m1.Cmp(m2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// we don't need to compare refresh, since those can always be merged...
|
||||
|
||||
// compare meta params for resources with auto edges
|
||||
r1e, ok1 := r1.(EdgeableRes)
|
||||
r2e, ok2 := r2.(EdgeableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("edgeable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1e.AutoEdgeMeta().Cmp(r2e.AutoEdgeMeta()) != nil {
|
||||
return fmt.Errorf("autoedge differs")
|
||||
}
|
||||
}
|
||||
|
||||
// compare meta params for resources with auto grouping
|
||||
r1g, ok1 := r1.(GroupableRes)
|
||||
r2g, ok2 := r2.(GroupableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("groupable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1g.AutoGroupMeta().Cmp(r2g.AutoGroupMeta()) != nil {
|
||||
return fmt.Errorf("autogroup differs")
|
||||
}
|
||||
|
||||
// if resources are grouped, are the groups the same?
|
||||
if i, j := r1g.GetGroup(), r2g.GetGroup(); len(i) != len(j) {
|
||||
return fmt.Errorf("autogroup groups differ")
|
||||
} else if len(i) > 0 { // trick the golinter
|
||||
|
||||
// Sort works with Res, so convert the lists to that
|
||||
iRes := []Res{}
|
||||
for _, r := range i {
|
||||
res := r.(Res)
|
||||
iRes = append(iRes, res)
|
||||
}
|
||||
jRes := []Res{}
|
||||
for _, r := range j {
|
||||
res := r.(Res)
|
||||
jRes = append(jRes, res)
|
||||
}
|
||||
|
||||
ix, jx := Sort(iRes), Sort(jRes) // now sort :)
|
||||
for k := range ix {
|
||||
// compare sub resources
|
||||
// TODO: should we use AdaptCmp here?
|
||||
// TODO: how would they run `Merge` ? (we don't)
|
||||
// this code path will probably not run, because
|
||||
// it is called in the lang before autogrouping!
|
||||
if err := ResCmp(ix[k], jx[k]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r1r, ok1 := r1.(RecvableRes)
|
||||
r2r, ok2 := r2.(RecvableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("recvable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
v1 := r1r.Recv()
|
||||
v2 := r2r.Recv()
|
||||
|
||||
if (v1 == nil) != (v2 == nil) { // xor
|
||||
return fmt.Errorf("recv params differ")
|
||||
}
|
||||
if v1 != nil && v2 != nil {
|
||||
// TODO: until we hit this code path, don't allow
|
||||
// adapting anything that has this set to non-zero
|
||||
if len(v1) != 0 || len(v2) != 0 {
|
||||
return fmt.Errorf("recv params exist")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r1s, ok1 := r1.(SendableRes)
|
||||
r2s, ok2 := r2.(SendableRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("sendable differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
s1 := r1s.Sent()
|
||||
s2 := r2s.Sent()
|
||||
|
||||
if (s1 == nil) != (s2 == nil) { // xor
|
||||
return fmt.Errorf("send params differ")
|
||||
}
|
||||
if s1 != nil && s2 != nil {
|
||||
// TODO: until we hit this code path, don't allow
|
||||
// adapting anything that has this set to non-nil
|
||||
return fmt.Errorf("send params exist")
|
||||
}
|
||||
}
|
||||
|
||||
// compare meta params for resources with reversible traits
|
||||
r1v, ok1 := r1.(ReversibleRes)
|
||||
r2v, ok2 := r2.(ReversibleRes)
|
||||
if ok1 != ok2 {
|
||||
return fmt.Errorf("reversible differs") // they must be different (optional)
|
||||
}
|
||||
if ok1 && ok2 {
|
||||
if r1v.ReversibleMeta().Cmp(r2v.ReversibleMeta()) != nil {
|
||||
return fmt.Errorf("reversible differs")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VertexCmpFn returns if two vertices are equivalent. It errors if they can't
|
||||
// be compared because one is not a vertex. This returns true if equal.
|
||||
// TODO: shouldn't the first argument be an `error` instead?
|
||||
func VertexCmpFn(v1, v2 pgraph.Vertex) (bool, error) {
|
||||
r1, ok := v1.(Res)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("v1 is not a Res")
|
||||
}
|
||||
r2, ok := v2.(Res)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("v2 is not a Res")
|
||||
}
|
||||
|
||||
if ResCmp(r1, r2) != nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// EdgeCmpFn returns if two edges are equivalent. It errors if they can't be
|
||||
// compared because one is not an edge. This returns true if equal.
|
||||
// TODO: shouldn't the first argument be an `error` instead?
|
||||
func EdgeCmpFn(e1, e2 pgraph.Edge) (bool, error) {
|
||||
edge1, ok := e1.(*Edge)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("e1 is not an Edge")
|
||||
}
|
||||
edge2, ok := e2.(*Edge)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("e2 is not an Edge")
|
||||
}
|
||||
return edge1.Cmp(edge2) == nil, nil
|
||||
}
|
||||
170
engine/copy.go
Normal file
170
engine/copy.go
Normal file
@@ -0,0 +1,170 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// ResCopy copies a resource. This is the main entry point for copying a
|
||||
// resource since it does all the common engine-level copying as well.
|
||||
func ResCopy(r CopyableRes) (CopyableRes, error) {
|
||||
res := r.Copy()
|
||||
res.SetKind(r.Kind())
|
||||
res.SetName(r.Name())
|
||||
|
||||
if x, ok := r.(MetaRes); ok {
|
||||
dst, ok := res.(MetaRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("meta interfaces are illogical")
|
||||
}
|
||||
dst.SetMetaParams(x.MetaParams().Copy()) // copy b/c we have it
|
||||
}
|
||||
|
||||
if x, ok := r.(RefreshableRes); ok {
|
||||
dst, ok := res.(RefreshableRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("refresh interfaces are illogical")
|
||||
}
|
||||
dst.SetRefresh(x.Refresh()) // no need to copy atm
|
||||
}
|
||||
|
||||
// copy meta params for resources with auto edges
|
||||
if x, ok := r.(EdgeableRes); ok {
|
||||
dst, ok := res.(EdgeableRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("autoedge interfaces are illogical")
|
||||
}
|
||||
dst.SetAutoEdgeMeta(x.AutoEdgeMeta()) // no need to copy atm
|
||||
}
|
||||
|
||||
// copy meta params for resources with auto grouping
|
||||
if x, ok := r.(GroupableRes); ok {
|
||||
dst, ok := res.(GroupableRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("autogroup interfaces are illogical")
|
||||
}
|
||||
dst.SetAutoGroupMeta(x.AutoGroupMeta()) // no need to copy atm
|
||||
|
||||
grouped := []GroupableRes{}
|
||||
for _, g := range x.GetGroup() {
|
||||
g0, ok := g.(CopyableRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("resource wasn't copyable")
|
||||
}
|
||||
g1, err := ResCopy(g0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g2, ok := g1.(GroupableRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("resource wasn't groupable")
|
||||
}
|
||||
grouped = append(grouped, g2)
|
||||
}
|
||||
dst.SetGroup(grouped)
|
||||
}
|
||||
|
||||
if x, ok := r.(RecvableRes); ok {
|
||||
dst, ok := res.(RecvableRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("recv interfaces are illogical")
|
||||
}
|
||||
dst.SetRecv(x.Recv()) // no need to copy atm
|
||||
}
|
||||
|
||||
if x, ok := r.(SendableRes); ok {
|
||||
dst, ok := res.(SendableRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("send interfaces are illogical")
|
||||
}
|
||||
if err := dst.Send(x.Sent()); err != nil { // no need to copy atm
|
||||
return nil, errwrap.Wrapf(err, "can't copy send")
|
||||
}
|
||||
}
|
||||
|
||||
// copy meta params for resources with reversible traits
|
||||
if x, ok := r.(ReversibleRes); ok {
|
||||
dst, ok := res.(ReversibleRes)
|
||||
if !ok {
|
||||
// programming error
|
||||
panic("reversible interfaces are illogical")
|
||||
}
|
||||
dst.SetReversibleMeta(x.ReversibleMeta()) // no need to copy atm
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ResMerge merges a set of resources that are compatible with each other. This
|
||||
// is the main entry point for the merging. They must each successfully be able
|
||||
// to run AdaptCmp without error.
|
||||
func ResMerge(r ...CompatibleRes) (CompatibleRes, error) {
|
||||
if len(r) == 0 {
|
||||
return nil, fmt.Errorf("zero resources given")
|
||||
}
|
||||
if len(r) == 1 {
|
||||
return r[0], nil
|
||||
}
|
||||
if len(r) > 2 {
|
||||
r0 := r[0]
|
||||
r1, err := ResMerge(r[1:]...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ResMerge(r0, r1)
|
||||
}
|
||||
// now we have r[0] and r[1] to merge here...
|
||||
r0 := r[0]
|
||||
r1 := r[1]
|
||||
if err := AdaptCmp(r0, r1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res, err := r0.Merge(r1) // resource method of this interface
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// meta should have come over in the copy
|
||||
|
||||
if x, ok := res.(RefreshableRes); ok {
|
||||
x0, ok0 := r0.(RefreshableRes)
|
||||
x1, ok1 := r1.(RefreshableRes)
|
||||
if !ok0 || !ok1 {
|
||||
// programming error
|
||||
panic("refresh interfaces are illogical")
|
||||
}
|
||||
|
||||
x.SetRefresh(x0.Refresh() || x1.Refresh()) // true if either is!
|
||||
}
|
||||
|
||||
// the other traits and metaparams can't be merged easily... so we don't
|
||||
// merge them, and if they were present and differed, and weren't copied
|
||||
// in the ResCopy method, then we should have errored above in AdaptCmp!
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,7 +15,11 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Edge is a struct that represents a graph's edge.
|
||||
type Edge struct {
|
||||
@@ -30,19 +34,19 @@ func (obj *Edge) String() string {
|
||||
return obj.Name
|
||||
}
|
||||
|
||||
// Compare returns true if two edges are equivalent. Otherwise it returns false.
|
||||
func (obj *Edge) Compare(edge *Edge) bool {
|
||||
// Cmp compares this edge to another. It returns nil if they are equivalent.
|
||||
func (obj *Edge) Cmp(edge *Edge) error {
|
||||
if obj.Name != edge.Name {
|
||||
return false
|
||||
return fmt.Errorf("edge names differ")
|
||||
}
|
||||
if obj.Notify != edge.Notify {
|
||||
return false
|
||||
return fmt.Errorf("notify values differ")
|
||||
}
|
||||
// FIXME: should we compare this as well?
|
||||
//if obj.refresh != edge.refresh {
|
||||
// return false
|
||||
// return fmt.Errorf("refresh values differ")
|
||||
//}
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh returns the pending refresh status of this edge.
|
||||
29
engine/error.go
Normal file
29
engine/error.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
// Error is a constant error type that implements error.
|
||||
type Error string
|
||||
|
||||
// Error fulfills the error interface of this type.
|
||||
func (e Error) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
// ErrClosed means we couldn't complete a task because we had closed.
|
||||
ErrClosed = Error("closed")
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,42 +15,14 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
package engine
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/purpleidea/mgmt/etcd/scheduler"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// World is an interface to the rest of the different graph state. It allows
|
||||
// the GAPI to store state and exchange information throughout the cluster. It
|
||||
// is the interface each machine uses to communicate with the rest of the world.
|
||||
type World interface { // TODO: is there a better name for this interface?
|
||||
ResWatch() chan error
|
||||
ResExport([]Res) error
|
||||
// FIXME: should this method take a "filter" data struct instead of many args?
|
||||
ResCollect(hostnameFilter, kindFilter []string) ([]Res, error)
|
||||
|
||||
StrWatch(namespace string) chan error
|
||||
StrIsNotExist(error) bool
|
||||
StrGet(namespace string) (string, error)
|
||||
StrSet(namespace, value string) error
|
||||
StrDel(namespace string) error
|
||||
|
||||
// XXX: add the exchange primitives in here directly?
|
||||
StrMapWatch(namespace string) chan error
|
||||
StrMapGet(namespace string) (map[string]string, error)
|
||||
StrMapSet(namespace, value string) error
|
||||
StrMapDel(namespace string) error
|
||||
|
||||
Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error)
|
||||
|
||||
Fs(uri string) (Fs, error)
|
||||
}
|
||||
|
||||
// from the ioutil package:
|
||||
// NopCloser(r io.Reader) io.ReadCloser // not implemented here
|
||||
// ReadAll(r io.Reader) ([]byte, error)
|
||||
@@ -76,7 +48,7 @@ type Fs interface {
|
||||
//IsDir(path string) (bool, error)
|
||||
//IsEmpty(path string) (bool, error)
|
||||
//NeuterAccents(s string) string
|
||||
//ReadAll(r io.Reader) ([]byte, error) // not needed
|
||||
//ReadAll(r io.Reader) ([]byte, error) // not needed, same as ioutil
|
||||
ReadDir(dirname string) ([]os.FileInfo, error)
|
||||
ReadFile(filename string) ([]byte, error)
|
||||
//SafeWriteReader(path string, r io.Reader) (err error)
|
||||
563
engine/graph/actions.go
Normal file
563
engine/graph/actions.go
Normal file
@@ -0,0 +1,563 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// OKTimestamp returns true if this vertex can run right now.
|
||||
func (obj *Engine) OKTimestamp(vertex pgraph.Vertex) bool {
|
||||
return len(obj.BadTimestamps(vertex)) == 0
|
||||
}
|
||||
|
||||
// BadTimestamps returns the list of vertices that are causing our timestamp to
|
||||
// be bad.
|
||||
func (obj *Engine) BadTimestamps(vertex pgraph.Vertex) []pgraph.Vertex {
|
||||
vs := []pgraph.Vertex{}
|
||||
ts := obj.state[vertex].timestamp
|
||||
// these are all the vertices pointing TO vertex, eg: ??? -> vertex
|
||||
for _, v := range obj.graph.IncomingGraphVertices(vertex) {
|
||||
// If the vertex has a greater timestamp than any prerequisite,
|
||||
// then we can't run right now. If they're equal (eg: initially
|
||||
// with a value of 0) then we also can't run because we should
|
||||
// let our pre-requisites go first.
|
||||
t := obj.state[v].timestamp
|
||||
if obj.Debug {
|
||||
obj.Logf("OKTimestamp: %d >= %d (%s): !%t", ts, t, v.String(), ts >= t)
|
||||
}
|
||||
if ts >= t {
|
||||
//return false
|
||||
vs = append(vs, v)
|
||||
}
|
||||
}
|
||||
return vs // formerly "true" if empty
|
||||
}
|
||||
|
||||
// Process is the primary function to execute a particular vertex in the graph.
|
||||
func (obj *Engine) Process(vertex pgraph.Vertex) error {
|
||||
res, isRes := vertex.(engine.Res)
|
||||
if !isRes {
|
||||
return fmt.Errorf("vertex is not a Res")
|
||||
}
|
||||
|
||||
// backpoke! (can be async)
|
||||
if vs := obj.BadTimestamps(vertex); len(vs) > 0 {
|
||||
// back poke in parallel (sync b/c of waitgroup)
|
||||
wg := &sync.WaitGroup{}
|
||||
for _, v := range obj.graph.IncomingGraphVertices(vertex) {
|
||||
if !pgraph.VertexContains(v, vs) { // only poke what's needed
|
||||
continue
|
||||
}
|
||||
|
||||
// doesn't really need to be in parallel, but we can...
|
||||
wg.Add(1)
|
||||
go func(vv pgraph.Vertex) {
|
||||
defer wg.Done()
|
||||
obj.state[vv].Poke() // async
|
||||
}(v)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
return nil // can't continue until timestamp is in sequence
|
||||
}
|
||||
|
||||
// semaphores!
|
||||
// These shouldn't ever block an exit, since the graph should eventually
|
||||
// converge causing their them to unlock. More interestingly, since they
|
||||
// run in a DAG alphabetically, there is no way to permanently deadlock,
|
||||
// assuming that resources individually don't ever block from finishing!
|
||||
// The exception is that semaphores with a zero count will always block!
|
||||
// TODO: Add a close mechanism to close/unblock zero count semaphores...
|
||||
semas := res.MetaParams().Sema
|
||||
if obj.Debug && len(semas) > 0 {
|
||||
obj.Logf("%s: Sema: P(%s)", res, strings.Join(semas, ", "))
|
||||
}
|
||||
if err := obj.semaLock(semas); err != nil { // lock
|
||||
// NOTE: in practice, this might not ever be truly necessary...
|
||||
return fmt.Errorf("shutdown of semaphores")
|
||||
}
|
||||
defer obj.semaUnlock(semas) // unlock
|
||||
if obj.Debug && len(semas) > 0 {
|
||||
defer obj.Logf("%s: Sema: V(%s)", res, strings.Join(semas, ", "))
|
||||
}
|
||||
|
||||
// sendrecv!
|
||||
// connect any senders to receivers and detect if values changed
|
||||
if res, ok := vertex.(engine.RecvableRes); ok {
|
||||
if updated, err := obj.SendRecv(res); err != nil {
|
||||
return errwrap.Wrapf(err, "could not SendRecv")
|
||||
} else if len(updated) > 0 {
|
||||
for _, changed := range updated {
|
||||
if changed { // at least one was updated
|
||||
// invalidate cache, mark as dirty
|
||||
obj.state[vertex].tuid.StopTimer()
|
||||
obj.state[vertex].isStateOK = false
|
||||
break
|
||||
}
|
||||
}
|
||||
// re-validate after we change any values
|
||||
if err := engine.Validate(res); err != nil {
|
||||
return errwrap.Wrapf(err, "failed Validate after SendRecv")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var ok = true
|
||||
var applied = false // did we run an apply?
|
||||
var noop = res.MetaParams().Noop // lookup the noop value
|
||||
var refresh bool
|
||||
var checkOK bool
|
||||
var err error
|
||||
|
||||
// lookup the refresh (notification) variable
|
||||
refresh = obj.RefreshPending(vertex) // do i need to perform a refresh?
|
||||
refreshableRes, isRefreshableRes := vertex.(engine.RefreshableRes)
|
||||
if isRefreshableRes {
|
||||
refreshableRes.SetRefresh(refresh) // tell the resource
|
||||
}
|
||||
|
||||
// Check cached state, to skip CheckApply, but can't skip if refreshing!
|
||||
// If the resource doesn't implement refresh, skip the refresh test.
|
||||
// FIXME: if desired, check that we pass through refresh notifications!
|
||||
if (!refresh || !isRefreshableRes) && obj.state[vertex].isStateOK {
|
||||
checkOK, err = true, nil
|
||||
|
||||
} else if noop && (refresh && isRefreshableRes) { // had a refresh to do w/ noop!
|
||||
checkOK, err = false, nil // therefore the state is wrong
|
||||
|
||||
// run the CheckApply!
|
||||
} else {
|
||||
obj.Logf("%s: CheckApply(%t)", res, !noop)
|
||||
// if this fails, don't UpdateTimestamp()
|
||||
checkOK, err = res.CheckApply(!noop)
|
||||
obj.Logf("%s: CheckApply(%t): Return(%t, %+v)", res, !noop, checkOK, err)
|
||||
}
|
||||
|
||||
if checkOK && err != nil { // should never return this way
|
||||
return fmt.Errorf("%s: resource programming error: CheckApply(%t): %t, %+v", res, !noop, checkOK, err)
|
||||
}
|
||||
|
||||
if !checkOK { // something changed, restart timer
|
||||
obj.state[vertex].cuid.ResetTimer() // activity!
|
||||
if obj.Debug {
|
||||
obj.Logf("%s: converger: reset timer", res)
|
||||
}
|
||||
}
|
||||
|
||||
// if CheckApply ran without noop and without error, state should be good
|
||||
if !noop && err == nil { // aka !noop || checkOK
|
||||
obj.state[vertex].tuid.StartTimer()
|
||||
obj.state[vertex].isStateOK = true // reset
|
||||
if refresh {
|
||||
obj.SetUpstreamRefresh(vertex, false) // refresh happened, clear the request
|
||||
if isRefreshableRes {
|
||||
refreshableRes.SetRefresh(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !checkOK { // if state *was* not ok, we had to have apply'ed
|
||||
if err != nil { // error during check or apply
|
||||
ok = false
|
||||
} else {
|
||||
applied = true
|
||||
}
|
||||
}
|
||||
|
||||
// when noop is true we always want to update timestamp
|
||||
if noop && err == nil {
|
||||
ok = true
|
||||
}
|
||||
|
||||
if ok {
|
||||
// did we actually do work?
|
||||
activity := applied
|
||||
if noop {
|
||||
activity = false // no we didn't do work...
|
||||
}
|
||||
|
||||
if activity { // add refresh flag to downstream edges...
|
||||
obj.SetDownstreamRefresh(vertex, true)
|
||||
}
|
||||
|
||||
// poke! (should (must?) be sync)
|
||||
wg := &sync.WaitGroup{}
|
||||
// update this timestamp *before* we poke or the poked
|
||||
// nodes might fail due to having a too old timestamp!
|
||||
obj.state[vertex].timestamp = time.Now().UnixNano() // update timestamp
|
||||
for _, v := range obj.graph.OutgoingGraphVertices(vertex) {
|
||||
if !obj.OKTimestamp(v) {
|
||||
// there is at least another one that will poke this...
|
||||
continue
|
||||
}
|
||||
|
||||
// If we're pausing (or exiting) then we can skip poking
|
||||
// so that the graph doesn't go on running forever until
|
||||
// it's completely done. This is an optional feature and
|
||||
// we can select it via ^C on user exit or via the GAPI.
|
||||
if obj.fastPause {
|
||||
obj.Logf("%s: fast pausing, poke skipped", res)
|
||||
continue
|
||||
}
|
||||
|
||||
// poke each vertex individually, in parallel...
|
||||
wg.Add(1)
|
||||
go func(vv pgraph.Vertex) {
|
||||
defer wg.Done()
|
||||
obj.state[vv].Poke()
|
||||
}(v)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
return errwrap.Wrapf(err, "error during Process()")
|
||||
}
|
||||
|
||||
// Worker is the common run frontend of the vertex. It handles all of the retry
|
||||
// and retry delay common code, and ultimately returns the final status of this
|
||||
// vertex execution. This function cannot be "re-run" for the same vertex. The
|
||||
// retry mechanism stuff happens inside of this. To actually "re-run" you need
|
||||
// to remove the vertex and build a new one. The engine guarantees that we do
|
||||
// not allow CheckApply to run while we are paused. That is enforced here.
|
||||
func (obj *Engine) Worker(vertex pgraph.Vertex) error {
|
||||
res, isRes := vertex.(engine.Res)
|
||||
if !isRes {
|
||||
return fmt.Errorf("vertex is not a resource")
|
||||
}
|
||||
|
||||
// bonus safety check
|
||||
if res.MetaParams().Burst == 0 && !(res.MetaParams().Limit == rate.Inf) { // blocked
|
||||
return fmt.Errorf("permanently limited (rate != Inf, burst = 0)")
|
||||
}
|
||||
|
||||
//defer close(obj.state[vertex].stopped) // done signal
|
||||
|
||||
obj.state[vertex].cuid = obj.Converger.Register()
|
||||
obj.state[vertex].tuid = obj.Converger.Register()
|
||||
// must wait for all users of the cuid to finish *before* we unregister!
|
||||
// as a result, this defer happens *before* the below wait group Wait...
|
||||
defer obj.state[vertex].cuid.Unregister()
|
||||
defer obj.state[vertex].tuid.Unregister()
|
||||
|
||||
defer obj.state[vertex].wg.Wait() // this Worker is the last to exit!
|
||||
|
||||
obj.state[vertex].wg.Add(1)
|
||||
go func() {
|
||||
defer obj.state[vertex].wg.Done()
|
||||
defer close(obj.state[vertex].eventsChan) // we close this on behalf of res
|
||||
|
||||
// This is a close reverse-multiplexer. If any of the channels
|
||||
// close, then it will cause the doneChan to close. That way,
|
||||
// multiple different folks can send a close signal, without
|
||||
// every worrying about duplicate channel close panics.
|
||||
obj.state[vertex].wg.Add(1)
|
||||
go func() {
|
||||
defer obj.state[vertex].wg.Done()
|
||||
|
||||
// reverse-multiplexer: any close, causes *the* close!
|
||||
select {
|
||||
case <-obj.state[vertex].processDone:
|
||||
case <-obj.state[vertex].watchDone:
|
||||
case <-obj.state[vertex].limitDone:
|
||||
case <-obj.state[vertex].removeDone:
|
||||
case <-obj.state[vertex].eventsDone:
|
||||
}
|
||||
|
||||
// the main "done" signal gets activated here!
|
||||
close(obj.state[vertex].doneChan)
|
||||
}()
|
||||
|
||||
var err error
|
||||
var retry = res.MetaParams().Retry // lookup the retry value
|
||||
var delay uint64
|
||||
for { // retry loop
|
||||
// a retry-delay was requested, wait, but don't block events!
|
||||
if delay > 0 {
|
||||
errDelayExpired := engine.Error("delay exit")
|
||||
err = func() error { // slim watch main loop
|
||||
timer := time.NewTimer(time.Duration(delay) * time.Millisecond)
|
||||
defer obj.state[vertex].init.Logf("the Watch delay expired!")
|
||||
defer timer.Stop() // it's nice to cleanup
|
||||
for {
|
||||
select {
|
||||
case <-timer.C: // the wait is over
|
||||
return errDelayExpired // special
|
||||
|
||||
case <-obj.state[vertex].init.Done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
if err == errDelayExpired {
|
||||
delay = 0 // reset
|
||||
continue
|
||||
}
|
||||
} else if interval := res.MetaParams().Poll; interval > 0 { // poll instead of watching :(
|
||||
obj.state[vertex].cuid.StartTimer()
|
||||
err = obj.state[vertex].poll(interval)
|
||||
obj.state[vertex].cuid.StopTimer() // clean up nicely
|
||||
} else {
|
||||
obj.state[vertex].cuid.StartTimer()
|
||||
obj.Logf("Watch(%s)", vertex)
|
||||
err = res.Watch() // run the watch normally
|
||||
obj.Logf("Watch(%s): Exited(%+v)", vertex, err)
|
||||
obj.state[vertex].cuid.StopTimer() // clean up nicely
|
||||
}
|
||||
if err == nil { // || err == engine.ErrClosed
|
||||
return // exited cleanly, we're done
|
||||
}
|
||||
// we've got an error...
|
||||
delay = res.MetaParams().Delay
|
||||
|
||||
if retry < 0 { // infinite retries
|
||||
continue
|
||||
}
|
||||
if retry > 0 { // don't decrement past 0
|
||||
retry--
|
||||
obj.state[vertex].init.Logf("retrying Watch after %.4f seconds (%d left)", float64(delay)/1000, retry)
|
||||
continue
|
||||
}
|
||||
//if retry == 0 { // optional
|
||||
// err = errwrap.Wrapf(err, "permanent watch error")
|
||||
//}
|
||||
break // break out of this and send the error
|
||||
} // for retry loop
|
||||
|
||||
// this section sends an error...
|
||||
// If the CheckApply loop exits and THEN the Watch fails with an
|
||||
// error, then we'd be stuck here if exit signal didn't unblock!
|
||||
select {
|
||||
case obj.state[vertex].eventsChan <- errwrap.Wrapf(err, "watch failed"):
|
||||
// send
|
||||
}
|
||||
}()
|
||||
|
||||
// If this exits cleanly, we must unblock the reverse-multiplexer.
|
||||
// I think this additional close is unnecessary, but it's not harmful.
|
||||
defer close(obj.state[vertex].eventsDone) // causes doneChan to close
|
||||
limiter := rate.NewLimiter(res.MetaParams().Limit, res.MetaParams().Burst)
|
||||
var reserv *rate.Reservation
|
||||
var reterr error
|
||||
var failed bool // has Process permanently failed?
|
||||
Loop:
|
||||
for { // process loop
|
||||
select {
|
||||
case err, ok := <-obj.state[vertex].eventsChan: // read from watch channel
|
||||
if !ok {
|
||||
return reterr // we only return when chan closes
|
||||
}
|
||||
// If the Watch method exits with an error, then this
|
||||
// channel will get that error propagated to it, which
|
||||
// we then save so we can return it to the caller of us.
|
||||
if err != nil {
|
||||
failed = true
|
||||
close(obj.state[vertex].watchDone) // causes doneChan to close
|
||||
reterr = errwrap.Append(reterr, err) // permanent failure
|
||||
continue
|
||||
}
|
||||
if obj.Debug {
|
||||
obj.Logf("event received")
|
||||
}
|
||||
reserv = limiter.ReserveN(time.Now(), 1) // one event
|
||||
// reserv.OK() seems to always be true here!
|
||||
|
||||
case _, ok := <-obj.state[vertex].pokeChan: // read from buffered poke channel
|
||||
if !ok { // we never close it
|
||||
panic("unexpected close of poke channel")
|
||||
}
|
||||
if obj.Debug {
|
||||
obj.Logf("poke received")
|
||||
}
|
||||
reserv = nil // we didn't receive a real event here...
|
||||
}
|
||||
if failed { // don't Process anymore if we've already failed...
|
||||
continue Loop
|
||||
}
|
||||
|
||||
// drop redundant pokes
|
||||
for len(obj.state[vertex].pokeChan) > 0 {
|
||||
select {
|
||||
case <-obj.state[vertex].pokeChan:
|
||||
default:
|
||||
// race, someone else read one!
|
||||
}
|
||||
}
|
||||
|
||||
// pause if one was requested...
|
||||
select {
|
||||
case <-obj.state[vertex].pauseSignal: // channel closes
|
||||
// NOTE: If we allowed a doneChan below to let us out
|
||||
// of the resumeSignal wait, then we could loop around
|
||||
// and run this again, causing a panic. Instead of this
|
||||
// being made safe with a sync.Once, we instead run a
|
||||
// Resume() call inside of the vertexRemoveFn function,
|
||||
// which should unblock it when we're going to need to.
|
||||
obj.state[vertex].pausedAck.Ack() // send ack
|
||||
// we are paused now, and waiting for resume or exit...
|
||||
select {
|
||||
case <-obj.state[vertex].resumeSignal: // channel closes
|
||||
// resumed!
|
||||
// pass through to allow a Process to try to run
|
||||
// TODO: consider adding this fast pause here...
|
||||
//if obj.fastPause {
|
||||
// obj.Logf("fast pausing on resume")
|
||||
// continue
|
||||
//}
|
||||
}
|
||||
default:
|
||||
// no pause requested, keep going...
|
||||
}
|
||||
if failed { // don't Process anymore if we've already failed...
|
||||
continue Loop
|
||||
}
|
||||
|
||||
// limit delay
|
||||
d := time.Duration(0)
|
||||
if reserv != nil {
|
||||
d = reserv.DelayFrom(time.Now())
|
||||
}
|
||||
if reserv != nil && d > 0 { // delay
|
||||
obj.state[vertex].init.Logf("limited (rate: %v/sec, burst: %d, next: %v)", res.MetaParams().Limit, res.MetaParams().Burst, d)
|
||||
timer := time.NewTimer(time.Duration(d) * time.Millisecond)
|
||||
LimitWait:
|
||||
for {
|
||||
select {
|
||||
case <-timer.C: // the wait is over
|
||||
break LimitWait
|
||||
|
||||
// consume other events while we're waiting...
|
||||
case e, ok := <-obj.state[vertex].eventsChan: // read from watch channel
|
||||
if !ok {
|
||||
return reterr // we only return when chan closes
|
||||
}
|
||||
if e != nil {
|
||||
failed = true
|
||||
close(obj.state[vertex].limitDone) // causes doneChan to close
|
||||
reterr = errwrap.Append(reterr, e) // permanent failure
|
||||
break LimitWait
|
||||
}
|
||||
if obj.Debug {
|
||||
obj.Logf("event received in limit")
|
||||
}
|
||||
// TODO: does this get added in properly?
|
||||
limiter.ReserveN(time.Now(), 1) // one event
|
||||
}
|
||||
}
|
||||
timer.Stop() // it's nice to cleanup
|
||||
obj.state[vertex].init.Logf("rate limiting expired!")
|
||||
}
|
||||
if failed { // don't Process anymore if we've already failed...
|
||||
continue Loop
|
||||
}
|
||||
// end of limit delay
|
||||
|
||||
// retry...
|
||||
var err error
|
||||
var retry = res.MetaParams().Retry // lookup the retry value
|
||||
var delay uint64
|
||||
RetryLoop:
|
||||
for { // retry loop
|
||||
if delay > 0 {
|
||||
timer := time.NewTimer(time.Duration(delay) * time.Millisecond)
|
||||
RetryWait:
|
||||
for {
|
||||
select {
|
||||
case <-timer.C: // the wait is over
|
||||
break RetryWait
|
||||
|
||||
// consume other events while we're waiting...
|
||||
case e, ok := <-obj.state[vertex].eventsChan: // read from watch channel
|
||||
if !ok {
|
||||
return reterr // we only return when chan closes
|
||||
}
|
||||
if e != nil {
|
||||
failed = true
|
||||
close(obj.state[vertex].limitDone) // causes doneChan to close
|
||||
reterr = errwrap.Append(reterr, e) // permanent failure
|
||||
break RetryWait
|
||||
}
|
||||
if obj.Debug {
|
||||
obj.Logf("event received in retry")
|
||||
}
|
||||
// TODO: does this get added in properly?
|
||||
limiter.ReserveN(time.Now(), 1) // one event
|
||||
}
|
||||
}
|
||||
timer.Stop() // it's nice to cleanup
|
||||
delay = 0 // reset
|
||||
obj.state[vertex].init.Logf("the CheckApply delay expired!")
|
||||
}
|
||||
if failed { // don't Process anymore if we've already failed...
|
||||
continue Loop
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("Process(%s)", vertex)
|
||||
}
|
||||
err = obj.Process(vertex)
|
||||
if obj.Debug {
|
||||
obj.Logf("Process(%s): Return(%+v)", vertex, err)
|
||||
}
|
||||
if err == nil {
|
||||
break RetryLoop
|
||||
}
|
||||
// we've got an error...
|
||||
delay = res.MetaParams().Delay
|
||||
|
||||
if retry < 0 { // infinite retries
|
||||
continue
|
||||
}
|
||||
if retry > 0 { // don't decrement past 0
|
||||
retry--
|
||||
obj.state[vertex].init.Logf("retrying CheckApply after %.4f seconds (%d left)", float64(delay)/1000, retry)
|
||||
continue
|
||||
}
|
||||
//if retry == 0 { // optional
|
||||
// err = errwrap.Wrapf(err, "permanent process error")
|
||||
//}
|
||||
|
||||
// It is important that we shutdown the Watch loop if
|
||||
// this dies. If Process fails permanently, we ask it
|
||||
// to exit right here... (It happens when we loop...)
|
||||
failed = true
|
||||
close(obj.state[vertex].processDone) // causes doneChan to close
|
||||
reterr = errwrap.Append(reterr, err) // permanent failure
|
||||
continue
|
||||
|
||||
} // retry loop
|
||||
|
||||
// When this Process loop exits, it's because something has
|
||||
// caused Watch() to shutdown (even if it's our permanent
|
||||
// failure from Process), which caused this channel to close.
|
||||
// On or more exit signals are possible, and more than one can
|
||||
// happen simultaneously.
|
||||
|
||||
} // process loop
|
||||
|
||||
//return nil // unreachable
|
||||
}
|
||||
30
engine/graph/autoedge.go
Normal file
30
engine/graph/autoedge.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"github.com/purpleidea/mgmt/engine/graph/autoedge"
|
||||
)
|
||||
|
||||
// AutoEdge adds the automatic edges to the graph.
|
||||
func (obj *Engine) AutoEdge() error {
|
||||
logf := func(format string, v ...interface{}) {
|
||||
obj.Logf("autoedge: "+format, v...)
|
||||
}
|
||||
return autoedge.AutoEdge(obj.nextGraph, obj.Debug, logf)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,38 +15,89 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
package autoedge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
multierr "github.com/hashicorp/go-multierror"
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// The AutoEdge interface is used to implement the autoedges feature.
|
||||
type AutoEdge interface {
|
||||
Next() []ResUID // call to get list of edges to add
|
||||
Test([]bool) bool // call until false
|
||||
}
|
||||
// AutoEdge adds the automatic edges to the graph.
|
||||
func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
|
||||
logf("adding autoedges...")
|
||||
|
||||
// UIDExistsInUIDs wraps the IFF method when used with a list of UID's.
|
||||
func UIDExistsInUIDs(uid ResUID, uids []ResUID) bool {
|
||||
for _, u := range uids {
|
||||
if uid.IFF(u) {
|
||||
return true
|
||||
// initially get all of the autoedges to seek out all possible errors
|
||||
var err error
|
||||
autoEdgeObjMap := make(map[engine.EdgeableRes]engine.AutoEdge)
|
||||
sorted := []engine.EdgeableRes{}
|
||||
for _, v := range graph.VerticesSorted() {
|
||||
res, ok := v.(engine.EdgeableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if res.AutoEdgeMeta().Disabled { // skip if this res is disabled
|
||||
continue
|
||||
}
|
||||
sorted = append(sorted, res)
|
||||
}
|
||||
|
||||
for _, res := range sorted { // for each vertexes autoedges
|
||||
autoEdgeObj, e := res.AutoEdges()
|
||||
if e != nil {
|
||||
err = errwrap.Append(err, e) // collect all errors
|
||||
continue
|
||||
}
|
||||
if autoEdgeObj == nil {
|
||||
logf("no auto edges were found for: %s", res)
|
||||
continue // next vertex
|
||||
}
|
||||
autoEdgeObjMap[res] = autoEdgeObj // save for next loop
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the auto edges had errors")
|
||||
}
|
||||
|
||||
// now that we're guaranteed error free, we can modify the graph safely
|
||||
for _, res := range sorted { // stable sort order for determinism in logs
|
||||
autoEdgeObj, exists := autoEdgeObjMap[res]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
for { // while the autoEdgeObj has more uids to add...
|
||||
uids := autoEdgeObj.Next() // get some!
|
||||
if uids == nil {
|
||||
logf("the auto edge list is empty for: %s", res)
|
||||
break // inner loop
|
||||
}
|
||||
if debug {
|
||||
logf("autoedge: UIDS:")
|
||||
for i, u := range uids {
|
||||
logf("autoedge: UID%d: %v", i, u)
|
||||
}
|
||||
}
|
||||
|
||||
// match and add edges
|
||||
result := addEdgesByMatchingUIDS(res, uids, graph, debug, logf)
|
||||
|
||||
// report back, and find out if we should continue
|
||||
if !autoEdgeObj.Test(result) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
// It would be great to ensure we didn't add any graph cycles here, but
|
||||
// instead of checking now, we'll move the check into the main loop.
|
||||
return nil
|
||||
}
|
||||
|
||||
// addEdgesByMatchingUIDS adds edges to the vertex in a graph based on if it
|
||||
// matches a uid list.
|
||||
func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []bool {
|
||||
func addEdgesByMatchingUIDS(res engine.EdgeableRes, uids []engine.ResUID, graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) []bool {
|
||||
// search for edges and see what matches!
|
||||
var result []bool
|
||||
|
||||
@@ -54,29 +105,36 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
|
||||
for _, uid := range uids {
|
||||
var found = false
|
||||
// uid is a ResUID object
|
||||
for _, vv := range g.Vertices() { // search
|
||||
if v == vv { // skip self
|
||||
for _, v := range graph.Vertices() { // search
|
||||
r, ok := v.(engine.EdgeableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if b, ok := g.Value("debug"); ok && util.Bool(b) {
|
||||
log.Printf("Compile: AutoEdge: Match: %s with UID: %s", vv, uid)
|
||||
if r.AutoEdgeMeta().Disabled { // skip if this res is disabled
|
||||
continue
|
||||
}
|
||||
if res == r { // skip self
|
||||
continue
|
||||
}
|
||||
if debug {
|
||||
logf("autoedge: Match: %s with UID: %s", r, uid)
|
||||
}
|
||||
// we must match to an effective UID for the resource,
|
||||
// that is to say, the name value of a res is a helpful
|
||||
// handle, but it is not necessarily a unique identity!
|
||||
// remember, resources can return multiple UID's each!
|
||||
if UIDExistsInUIDs(uid, VtoR(vv).UIDs()) {
|
||||
// add edge from: vv -> v
|
||||
if UIDExistsInUIDs(uid, r.UIDs()) {
|
||||
// add edge from: r -> res
|
||||
if uid.IsReversed() {
|
||||
txt := fmt.Sprintf("AutoEdge: %s -> %s", vv, v)
|
||||
log.Printf("Compile: Adding %s", txt)
|
||||
edge := &Edge{Name: txt}
|
||||
g.AddEdge(vv, v, edge)
|
||||
txt := fmt.Sprintf("%s -> %s (autoedge)", r, res)
|
||||
logf("autoedge: adding: %s", txt)
|
||||
edge := &engine.Edge{Name: txt}
|
||||
graph.AddEdge(r, res, edge)
|
||||
} else { // edges go the "normal" way, eg: pkg resource
|
||||
txt := fmt.Sprintf("AutoEdge: %s -> %s", v, vv)
|
||||
log.Printf("Compile: Adding %s", txt)
|
||||
edge := &Edge{Name: txt}
|
||||
g.AddEdge(v, vv, edge)
|
||||
txt := fmt.Sprintf("%s -> %s (autoedge)", res, r)
|
||||
logf("autoedge: adding: %s", txt)
|
||||
edge := &engine.Edge{Name: txt}
|
||||
graph.AddEdge(res, r, edge)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
@@ -87,62 +145,12 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
|
||||
return result
|
||||
}
|
||||
|
||||
// AutoEdges adds the automatic edges to the graph.
|
||||
func AutoEdges(g *pgraph.Graph) error {
|
||||
log.Println("Compile: Adding AutoEdges...")
|
||||
|
||||
// initially get all of the autoedges to seek out all possible errors
|
||||
var err error
|
||||
autoEdgeObjVertexMap := make(map[pgraph.Vertex]AutoEdge)
|
||||
sorted := g.VerticesSorted()
|
||||
|
||||
for _, v := range sorted { // for each vertexes autoedges
|
||||
if !VtoR(v).Meta().AutoEdge { // is the metaparam true?
|
||||
continue
|
||||
}
|
||||
autoEdgeObj, e := VtoR(v).AutoEdges()
|
||||
if e != nil {
|
||||
err = multierr.Append(err, e) // collect all errors
|
||||
continue
|
||||
}
|
||||
if autoEdgeObj == nil {
|
||||
log.Printf("%s: No auto edges were found!", v)
|
||||
continue // next vertex
|
||||
}
|
||||
autoEdgeObjVertexMap[v] = autoEdgeObj // save for next loop
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the auto edges had errors")
|
||||
}
|
||||
|
||||
// now that we're guaranteed error free, we can modify the graph safely
|
||||
for _, v := range sorted { // stable sort order for determinism in logs
|
||||
autoEdgeObj, exists := autoEdgeObjVertexMap[v]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
for { // while the autoEdgeObj has more uids to add...
|
||||
uids := autoEdgeObj.Next() // get some!
|
||||
if uids == nil {
|
||||
log.Printf("%s: The auto edge list is empty!", v)
|
||||
break // inner loop
|
||||
}
|
||||
if b, ok := g.Value("debug"); ok && util.Bool(b) {
|
||||
log.Println("Compile: AutoEdge: UIDS:")
|
||||
for i, u := range uids {
|
||||
log.Printf("Compile: AutoEdge: UID%d: %v", i, u)
|
||||
}
|
||||
}
|
||||
|
||||
// match and add edges
|
||||
result := addEdgesByMatchingUIDS(g, v, uids)
|
||||
|
||||
// report back, and find out if we should continue
|
||||
if !autoEdgeObj.Test(result) {
|
||||
break
|
||||
}
|
||||
// UIDExistsInUIDs wraps the IFF method when used with a list of UID's.
|
||||
func UIDExistsInUIDs(uid engine.ResUID, uids []engine.ResUID) bool {
|
||||
for _, u := range uids {
|
||||
if uid.IFF(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
140
engine/graph/autogroup.go
Normal file
140
engine/graph/autogroup.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/graph/autogroup"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// AutoGroup runs the auto grouping on the loaded graph.
|
||||
func (obj *Engine) AutoGroup(ag engine.AutoGrouper) error {
|
||||
if obj.nextGraph == nil {
|
||||
return fmt.Errorf("there is no active graph to autogroup")
|
||||
}
|
||||
|
||||
logf := func(format string, v ...interface{}) {
|
||||
obj.Logf("autogroup: "+format, v...)
|
||||
}
|
||||
|
||||
// wrap ag with our own vertexCmp, vertexMerge and edgeMerge
|
||||
wrapped := &wrappedGrouper{
|
||||
AutoGrouper: ag, // pass in the existing autogrouper
|
||||
}
|
||||
|
||||
if err := autogroup.AutoGroup(wrapped, obj.nextGraph, obj.Debug, logf); err != nil {
|
||||
return errwrap.Wrapf(err, "autogrouping failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// wrappedGrouper is an autogrouper which adds our own Cmp and Merge functions
|
||||
// on top of the desired AutoGrouper that was specified.
|
||||
type wrappedGrouper struct {
|
||||
engine.AutoGrouper // anonymous interface
|
||||
}
|
||||
|
||||
func (obj *wrappedGrouper) Name() string {
|
||||
return fmt.Sprintf("wrappedGrouper: %s", obj.AutoGrouper.Name())
|
||||
}
|
||||
|
||||
func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
||||
// call existing vertexCmp first
|
||||
if err := obj.AutoGrouper.VertexCmp(v1, v2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r1, ok := v1.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("v1 is not a GroupableRes")
|
||||
}
|
||||
r2, ok := v2.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("v2 is not a GroupableRes")
|
||||
}
|
||||
|
||||
// Some resources of different kinds can now group together!
|
||||
//if r1.Kind() != r2.Kind() { // we must group similar kinds
|
||||
// return fmt.Errorf("the two resources aren't the same kind")
|
||||
//}
|
||||
// someone doesn't want to group!
|
||||
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
|
||||
return fmt.Errorf("one of the autogroup flags is false")
|
||||
}
|
||||
|
||||
if r1.IsGrouped() { // already grouped!
|
||||
return fmt.Errorf("already grouped")
|
||||
}
|
||||
if len(r2.GetGroup()) > 0 { // already has children grouped!
|
||||
return fmt.Errorf("already has groups")
|
||||
}
|
||||
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
|
||||
return errwrap.Wrapf(err, "the GroupCmp failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *wrappedGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
|
||||
r1, ok := v1.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("v1 is not a GroupableRes")
|
||||
}
|
||||
r2, ok := v2.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("v2 is not a GroupableRes")
|
||||
}
|
||||
|
||||
if err = r1.GroupRes(r2); err != nil { // GroupRes skips stupid groupings
|
||||
return // return early on error
|
||||
}
|
||||
|
||||
// merging two resources into one should yield the sum of their semas
|
||||
if semas := r2.MetaParams().Sema; len(semas) > 0 {
|
||||
r1.MetaParams().Sema = append(r1.MetaParams().Sema, semas...)
|
||||
r1.MetaParams().Sema = util.StrRemoveDuplicatesInList(r1.MetaParams().Sema)
|
||||
}
|
||||
|
||||
return // success or fail, and no need to merge the actual vertices!
|
||||
}
|
||||
|
||||
func (obj *wrappedGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
e1x, ok := e1.(*engine.Edge)
|
||||
if !ok {
|
||||
return e2 // just return something to avoid needing to error
|
||||
}
|
||||
e2x, ok := e2.(*engine.Edge)
|
||||
if !ok {
|
||||
return e1 // just return something to avoid needing to error
|
||||
}
|
||||
|
||||
// TODO: should we merge the edge.Notify or edge.refresh values?
|
||||
edge := &engine.Edge{
|
||||
Notify: e1x.Notify || e2x.Notify, // TODO: should we merge this?
|
||||
}
|
||||
refresh := e1x.Refresh() || e2x.Refresh() // TODO: should we merge this?
|
||||
edge.SetRefresh(refresh)
|
||||
|
||||
return edge
|
||||
}
|
||||
73
engine/graph/autogroup/autogroup.go
Normal file
73
engine/graph/autogroup/autogroup.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// AutoGroup is the mechanical auto group "runner" that runs the interface spec.
|
||||
// TODO: this algorithm may not be correct in all cases. replace if needed!
|
||||
func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
|
||||
logf("algorithm: %s...", ag.Name())
|
||||
if err := ag.Init(g); err != nil {
|
||||
return errwrap.Wrapf(err, "error running autoGroup(init)")
|
||||
}
|
||||
|
||||
for {
|
||||
var v, w pgraph.Vertex
|
||||
v, w, err := ag.VertexNext() // get pair to compare
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
|
||||
}
|
||||
merged := false
|
||||
// save names since they change during the runs
|
||||
vStr := fmt.Sprintf("%v", v) // valid even if it is nil
|
||||
wStr := fmt.Sprintf("%v", w)
|
||||
|
||||
if err := ag.VertexCmp(v, w); err != nil { // cmp ?
|
||||
if debug {
|
||||
logf("!GroupCmp for: %s into: %s", wStr, vStr)
|
||||
}
|
||||
|
||||
// remove grouped vertex and merge edges (res is safe)
|
||||
} else if err := VertexMerge(g, v, w, ag.VertexMerge, ag.EdgeMerge); err != nil { // merge...
|
||||
logf("!VertexMerge for: %s into: %s", wStr, vStr)
|
||||
|
||||
} else { // success!
|
||||
logf("%s into %s", wStr, vStr)
|
||||
merged = true // woo
|
||||
}
|
||||
|
||||
// did these get used?
|
||||
if ok, err := ag.VertexTest(merged); err != nil {
|
||||
return errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
|
||||
} else if !ok {
|
||||
break // done!
|
||||
}
|
||||
}
|
||||
|
||||
// It would be great to ensure we didn't add any graph cycles here, but
|
||||
// instead of checking now, we'll move the check into the main loop.
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,7 +15,9 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
// +build !root
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,13 +27,113 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("nooptest", func() engine.Res { return &NoopResTest{} })
|
||||
}
|
||||
|
||||
// NoopResTest is a no-op resource that groups strangely.
|
||||
type NoopResTest struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Groupable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Comment string
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Default() engine.Res {
|
||||
return &NoopResTest{}
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Watch() error {
|
||||
return nil // not needed
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return true, nil // state is always okay
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) Cmp(r engine.Res) error {
|
||||
// we can only compare NoopRes to others of the same resource kind
|
||||
res, ok := r.(*NoopResTest)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Comment != res.Comment {
|
||||
return fmt.Errorf("comment differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) GroupCmp(r engine.GroupableRes) error {
|
||||
res, ok := r.(*NoopResTest)
|
||||
if !ok {
|
||||
return fmt.Errorf("resource is not the same kind")
|
||||
}
|
||||
|
||||
// TODO: implement this in vertexCmp for *testGrouper instead?
|
||||
if strings.Contains(res.Name(), ",") { // HACK
|
||||
return fmt.Errorf("already grouped") // element to be grouped is already grouped!
|
||||
}
|
||||
|
||||
// group if they start with the same letter! (helpful hack for testing)
|
||||
if obj.Name()[0] != res.Name()[0] {
|
||||
return fmt.Errorf("different starting letter")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewNoopResTest(name string) *NoopResTest {
|
||||
n, err := engine.NewNamedResource("nooptest", name)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("unexpected error: %+v", err))
|
||||
}
|
||||
|
||||
//x := n.(*resources.NoopRes)
|
||||
g, ok := n.(engine.GroupableRes)
|
||||
if !ok {
|
||||
panic("not a GroupableRes")
|
||||
}
|
||||
g.AutoGroupMeta().Disabled = false // always autogroup
|
||||
|
||||
//x := g.(*NoopResTest)
|
||||
x := n.(*NoopResTest)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
func NewNoopResTestSema(name string, semas []string) *NoopResTest {
|
||||
n := NewNoopResTest(name)
|
||||
n.MetaParams().Sema = semas
|
||||
return n
|
||||
}
|
||||
|
||||
// NE is a helper function to make testing easier. It creates a new noop edge.
|
||||
func NE(s string) pgraph.Edge {
|
||||
obj := &Edge{Name: s}
|
||||
obj := &engine.Edge{Name: s}
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -40,41 +142,96 @@ type testGrouper struct {
|
||||
NonReachabilityGrouper // "inherit" what we want, and reimplement the rest
|
||||
}
|
||||
|
||||
func (ag *testGrouper) name() string {
|
||||
func (obj *testGrouper) Name() string {
|
||||
return "testGrouper"
|
||||
}
|
||||
|
||||
func (ag *testGrouper) vertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
|
||||
if err := VtoR(v1).GroupRes(VtoR(v2)); err != nil { // group them first
|
||||
func (obj *testGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
||||
// call existing vertexCmp first
|
||||
if err := obj.NonReachabilityGrouper.VertexCmp(v1, v2); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r1, ok := v1.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("v1 is not a GroupableRes")
|
||||
}
|
||||
r2, ok := v2.(engine.GroupableRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("v2 is not a GroupableRes")
|
||||
}
|
||||
|
||||
if r1.Kind() != r2.Kind() { // we must group similar kinds
|
||||
// TODO: maybe future resources won't need this limitation?
|
||||
return fmt.Errorf("the two resources aren't the same kind")
|
||||
}
|
||||
// someone doesn't want to group!
|
||||
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
|
||||
return fmt.Errorf("one of the autogroup flags is false")
|
||||
}
|
||||
|
||||
if r1.IsGrouped() { // already grouped!
|
||||
return fmt.Errorf("already grouped")
|
||||
}
|
||||
if len(r2.GetGroup()) > 0 { // already has children grouped!
|
||||
return fmt.Errorf("already has groups")
|
||||
}
|
||||
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
|
||||
return errwrap.Wrapf(err, "the GroupCmp failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *testGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
|
||||
r1 := v1.(engine.GroupableRes)
|
||||
r2 := v2.(engine.GroupableRes)
|
||||
if err := r1.GroupRes(r2); err != nil { // group them first
|
||||
return nil, err
|
||||
}
|
||||
// HACK: update the name so it matches full list of self+grouped
|
||||
obj := VtoR(v1)
|
||||
names := strings.Split(obj.GetName(), ",") // load in stored names
|
||||
for _, n := range obj.GetGroup() {
|
||||
names = append(names, n.GetName()) // add my contents
|
||||
res := v1.(engine.GroupableRes)
|
||||
names := strings.Split(res.Name(), ",") // load in stored names
|
||||
for _, n := range res.GetGroup() {
|
||||
names = append(names, n.Name()) // add my contents
|
||||
}
|
||||
names = util.StrRemoveDuplicatesInList(names) // remove duplicates
|
||||
sort.Strings(names)
|
||||
obj.SetName(strings.Join(names, ","))
|
||||
res.SetName(strings.Join(names, ","))
|
||||
|
||||
// TODO: copied from autogroup.go, so try and build a better test...
|
||||
// merging two resources into one should yield the sum of their semas
|
||||
if semas := r2.MetaParams().Sema; len(semas) > 0 {
|
||||
r1.MetaParams().Sema = append(r1.MetaParams().Sema, semas...)
|
||||
r1.MetaParams().Sema = util.StrRemoveDuplicatesInList(r1.MetaParams().Sema)
|
||||
}
|
||||
|
||||
return // success or fail, and no need to merge the actual vertices!
|
||||
}
|
||||
|
||||
func (ag *testGrouper) edgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
edge1 := e1.(*Edge) // panic if wrong
|
||||
edge2 := e2.(*Edge) // panic if wrong
|
||||
func (obj *testGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
edge1 := e1.(*engine.Edge) // panic if wrong
|
||||
edge2 := e2.(*engine.Edge) // panic if wrong
|
||||
// HACK: update the name so it makes a union of both names
|
||||
n1 := strings.Split(edge1.Name, ",") // load
|
||||
n2 := strings.Split(edge2.Name, ",") // load
|
||||
names := append(n1, n2...)
|
||||
names = util.StrRemoveDuplicatesInList(names) // remove duplicates
|
||||
sort.Strings(names)
|
||||
return &Edge{Name: strings.Join(names, ",")}
|
||||
return &engine.Edge{Name: strings.Join(names, ",")}
|
||||
}
|
||||
|
||||
// helper function
|
||||
func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) {
|
||||
AutoGroup(g1, &testGrouper{}) // edits the graph
|
||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||
logf := func(format string, v ...interface{}) {
|
||||
t.Logf("test: "+format, v...)
|
||||
}
|
||||
|
||||
if err := AutoGroup(&testGrouper{}, g1, debug, logf); err != nil { // edits the graph
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
err := GraphCmp(g1, g2)
|
||||
if err != nil {
|
||||
t.Logf(" actual (g1): %v%v", g1, fullPrint(g1))
|
||||
@@ -84,40 +241,6 @@ func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) {
|
||||
}
|
||||
}
|
||||
|
||||
type NoopResTest struct {
|
||||
NoopRes
|
||||
}
|
||||
|
||||
func (obj *NoopResTest) GroupCmp(r Res) bool {
|
||||
res, ok := r.(*NoopResTest)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: implement this in vertexCmp for *testGrouper instead?
|
||||
if strings.Contains(res.Name, ",") { // HACK
|
||||
return false // element to be grouped is already grouped!
|
||||
}
|
||||
|
||||
// group if they start with the same letter! (helpful hack for testing)
|
||||
return obj.Name[0] == res.Name[0]
|
||||
}
|
||||
|
||||
func NewNoopResTest(name string) *NoopResTest {
|
||||
obj := &NoopResTest{
|
||||
NoopRes: NoopRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: name,
|
||||
Kind: "noop",
|
||||
MetaParams: MetaParams{
|
||||
AutoGroup: true, // always autogroup
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// GraphCmp compares the topology of two graphs and returns nil if they're
|
||||
// equal. It also compares if grouped element groups are identical.
|
||||
// TODO: port this to use the pgraph.GraphCmp function instead.
|
||||
@@ -133,20 +256,20 @@ func GraphCmp(g1, g2 *pgraph.Graph) error {
|
||||
Loop:
|
||||
// check vertices
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
|
||||
l1 := strings.Split(VtoR(v1).GetName(), ",") // make list of everyone's names...
|
||||
for _, x1 := range VtoR(v1).GetGroup() {
|
||||
l1 = append(l1, x1.GetName()) // add my contents
|
||||
r1 := v1.(engine.GroupableRes)
|
||||
l1 := strings.Split(r1.Name(), ",") // make list of everyone's names...
|
||||
for _, x1 := range r1.GetGroup() {
|
||||
l1 = append(l1, x1.Name()) // add my contents
|
||||
}
|
||||
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
|
||||
sort.Strings(l1)
|
||||
|
||||
// inner loop
|
||||
for v2 := range g2.Adjacency() { // does it match in g2 ?
|
||||
|
||||
l2 := strings.Split(VtoR(v2).GetName(), ",")
|
||||
for _, x2 := range VtoR(v2).GetGroup() {
|
||||
l2 = append(l2, x2.GetName())
|
||||
r2 := v2.(engine.GroupableRes)
|
||||
l2 := strings.Split(r2.Name(), ",")
|
||||
for _, x2 := range r2.GetGroup() {
|
||||
l2 = append(l2, x2.Name())
|
||||
}
|
||||
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
|
||||
sort.Strings(l2)
|
||||
@@ -157,7 +280,7 @@ Loop:
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("graph g1, has no match in g2 for: %v", VtoR(v1).GetName())
|
||||
return fmt.Errorf("graph g1, has no match in g2 for: %v", r1.Name())
|
||||
}
|
||||
// vertices (and groups) match :)
|
||||
|
||||
@@ -166,35 +289,40 @@ Loop:
|
||||
v2 := m[v1] // lookup in map to get correspondance
|
||||
// g1.Adjacency()[v1] corresponds to g2.Adjacency()[v2]
|
||||
if e1, e2 := len(g1.Adjacency()[v1]), len(g2.Adjacency()[v2]); e1 != e2 {
|
||||
return fmt.Errorf("graph g1, vertex(%v) has %d edges, while g2, vertex(%v) has %d", VtoR(v1).GetName(), e1, VtoR(v2).GetName(), e2)
|
||||
r1 := v1.(engine.Res)
|
||||
r2 := v2.(engine.Res)
|
||||
return fmt.Errorf("graph g1, vertex(%v) has %d edges, while g2, vertex(%v) has %d", r1.Name(), e1, r2.Name(), e2)
|
||||
}
|
||||
|
||||
for vv1, ee1 := range g1.Adjacency()[v1] {
|
||||
vv2 := m[vv1]
|
||||
ee1 := ee1.(*Edge)
|
||||
ee2 := g2.Adjacency()[v2][vv2].(*Edge)
|
||||
ee1 := ee1.(*engine.Edge)
|
||||
ee2 := g2.Adjacency()[v2][vv2].(*engine.Edge)
|
||||
|
||||
// these are edges from v1 -> vv1 via ee1 (graph 1)
|
||||
// to cmp to edges from v2 -> vv2 via ee2 (graph 2)
|
||||
|
||||
// check: (1) vv1 == vv2 ? (we've already checked this!)
|
||||
l1 := strings.Split(VtoR(vv1).GetName(), ",") // make list of everyone's names...
|
||||
for _, x1 := range VtoR(vv1).GetGroup() {
|
||||
l1 = append(l1, x1.GetName()) // add my contents
|
||||
rr1 := vv1.(engine.GroupableRes)
|
||||
rr2 := vv2.(engine.GroupableRes)
|
||||
|
||||
l1 := strings.Split(rr1.Name(), ",") // make list of everyone's names...
|
||||
for _, x1 := range rr1.GetGroup() {
|
||||
l1 = append(l1, x1.Name()) // add my contents
|
||||
}
|
||||
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
|
||||
sort.Strings(l1)
|
||||
|
||||
l2 := strings.Split(VtoR(vv2).GetName(), ",")
|
||||
for _, x2 := range VtoR(vv2).GetGroup() {
|
||||
l2 = append(l2, x2.GetName())
|
||||
l2 := strings.Split(rr2.Name(), ",")
|
||||
for _, x2 := range rr2.GetGroup() {
|
||||
l2 = append(l2, x2.Name())
|
||||
}
|
||||
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
|
||||
sort.Strings(l2)
|
||||
|
||||
// does l1 match l2 ?
|
||||
if !ListStrCmp(l1, l2) { // cmp!
|
||||
return fmt.Errorf("graph g1 and g2 don't agree on: %v and %v", VtoR(vv1).GetName(), VtoR(vv2).GetName())
|
||||
return fmt.Errorf("graph g1 and g2 don't agree on: %v and %v", rr1.Name(), rr2.Name())
|
||||
}
|
||||
|
||||
// check: (2) ee1 == ee2
|
||||
@@ -207,11 +335,13 @@ Loop:
|
||||
// check meta parameters
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
for v2 := range g2.Adjacency() { // does it match in g2 ?
|
||||
s1, s2 := VtoR(v1).Meta().Sema, VtoR(v2).Meta().Sema
|
||||
r1 := v1.(engine.Res)
|
||||
r2 := v2.(engine.Res)
|
||||
s1, s2 := r1.MetaParams().Sema, r2.MetaParams().Sema
|
||||
sort.Strings(s1)
|
||||
sort.Strings(s2)
|
||||
if !reflect.DeepEqual(s1, s2) {
|
||||
return fmt.Errorf("vertex %s and vertex %s have different semaphores", VtoR(v1).GetName(), VtoR(v2).GetName())
|
||||
return fmt.Errorf("vertex %s and vertex %s have different semaphores", r1.Name(), r2.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -242,17 +372,20 @@ func ListStrCmp(a, b []string) bool {
|
||||
func fullPrint(g *pgraph.Graph) (str string) {
|
||||
str += "\n"
|
||||
for v := range g.Adjacency() {
|
||||
if semas := VtoR(v).Meta().Sema; len(semas) > 0 {
|
||||
str += fmt.Sprintf("* v: %v; sema: %v\n", VtoR(v).GetName(), semas)
|
||||
r := v.(engine.Res)
|
||||
if semas := r.MetaParams().Sema; len(semas) > 0 {
|
||||
str += fmt.Sprintf("* v: %v; sema: %v\n", r.Name(), semas)
|
||||
} else {
|
||||
str += fmt.Sprintf("* v: %v\n", VtoR(v).GetName())
|
||||
str += fmt.Sprintf("* v: %v\n", r.Name())
|
||||
}
|
||||
// TODO: add explicit grouping data?
|
||||
}
|
||||
for v1 := range g.Adjacency() {
|
||||
for v2, e := range g.Adjacency()[v1] {
|
||||
edge := e.(*Edge)
|
||||
str += fmt.Sprintf("* e: %v -> %v # %v\n", VtoR(v1).GetName(), VtoR(v2).GetName(), edge.Name)
|
||||
r1 := v1.(engine.Res)
|
||||
r2 := v2.(engine.Res)
|
||||
edge := e.(*engine.Edge)
|
||||
str += fmt.Sprintf("* e: %v -> %v # %v\n", r1.Name(), r2.Name(), edge.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -462,10 +595,11 @@ func TestPgraphGrouping11(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// simple merge 1
|
||||
/* simple merge 1
|
||||
// a1 a2 a1,a2
|
||||
// \ / >>> | (arrows point downwards)
|
||||
// b b
|
||||
*/
|
||||
func TestPgraphGrouping12(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -487,10 +621,11 @@ func TestPgraphGrouping12(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// simple merge 2
|
||||
/* simple merge 2
|
||||
// b b
|
||||
// / \ >>> | (arrows point downwards)
|
||||
// a1 a2 a1,a2
|
||||
*/
|
||||
func TestPgraphGrouping13(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -512,10 +647,11 @@ func TestPgraphGrouping13(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// triple merge
|
||||
/* triple merge
|
||||
// a1 a2 a3 a1,a2,a3
|
||||
// \ | / >>> | (arrows point downwards)
|
||||
// b b
|
||||
*/
|
||||
func TestPgraphGrouping14(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -540,12 +676,13 @@ func TestPgraphGrouping14(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// chain merge
|
||||
/* chain merge
|
||||
// a1 a1
|
||||
// / \ |
|
||||
// b1 b2 >>> b1,b2 (arrows point downwards)
|
||||
// \ / |
|
||||
// c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping15(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -575,7 +712,7 @@ func TestPgraphGrouping15(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// re-attach 1 (outer)
|
||||
/* re-attach 1 (outer)
|
||||
// technically the second possibility is valid too, depending on which order we
|
||||
// merge edges in, and if we don't filter out any unnecessary edges afterwards!
|
||||
// a1 a2 a1,a2 a1,a2
|
||||
@@ -583,6 +720,7 @@ func TestPgraphGrouping15(t *testing.T) {
|
||||
// b1 / >>> b1 OR b1 / (arrows point downwards)
|
||||
// | / | | /
|
||||
// c1 c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping16(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -610,12 +748,13 @@ func TestPgraphGrouping16(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// re-attach 2 (inner)
|
||||
/* re-attach 2 (inner)
|
||||
// a1 b2 a1
|
||||
// | / |
|
||||
// b1 / >>> b1,b2 (arrows point downwards)
|
||||
// | / |
|
||||
// c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping17(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -643,13 +782,14 @@ func TestPgraphGrouping17(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// re-attach 3 (double)
|
||||
/* re-attach 3 (double)
|
||||
// similar to "re-attach 1", technically there is a second possibility for this
|
||||
// a2 a1 b2 a1,a2
|
||||
// \ | / |
|
||||
// \ b1 / >>> b1,b2 (arrows point downwards)
|
||||
// \ | / |
|
||||
// c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping18(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -680,10 +820,11 @@ func TestPgraphGrouping18(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// connected merge 0, (no change!)
|
||||
/* connected merge 0, (no change!)
|
||||
// a1 a1
|
||||
// \ >>> \ (arrows point downwards)
|
||||
// a2 a2
|
||||
*/
|
||||
func TestPgraphGroupingConnected0(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -702,12 +843,13 @@ func TestPgraphGroupingConnected0(t *testing.T) {
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// connected merge 1, (no change!)
|
||||
/* connected merge 1, (no change!)
|
||||
// a1 a1
|
||||
// \ \
|
||||
// b >>> b (arrows point downwards)
|
||||
// \ \
|
||||
// a2 a2
|
||||
*/
|
||||
func TestPgraphGroupingConnected1(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
@@ -731,3 +873,57 @@ func TestPgraphGroupingConnected1(t *testing.T) {
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
func TestPgraphSemaphoreGrouping1(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTestSema("a1", []string{"s:1"})
|
||||
a2 := NewNoopResTestSema("a2", []string{"s:2"})
|
||||
a3 := NewNoopResTestSema("a3", []string{"s:3"})
|
||||
g1.AddVertex(a1)
|
||||
g1.AddVertex(a2)
|
||||
g1.AddVertex(a3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:1", "s:2", "s:3"})
|
||||
g2.AddVertex(a123)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
func TestPgraphSemaphoreGrouping2(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTestSema("a1", []string{"s:10", "s:11"})
|
||||
a2 := NewNoopResTestSema("a2", []string{"s:2"})
|
||||
a3 := NewNoopResTestSema("a3", []string{"s:3"})
|
||||
g1.AddVertex(a1)
|
||||
g1.AddVertex(a2)
|
||||
g1.AddVertex(a3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:10", "s:11", "s:2", "s:3"})
|
||||
g2.AddVertex(a123)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
func TestPgraphSemaphoreGrouping3(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTestSema("a1", []string{"s:1", "s:2"})
|
||||
a2 := NewNoopResTestSema("a2", []string{"s:2"})
|
||||
a3 := NewNoopResTestSema("a3", []string{"s:3"})
|
||||
g1.AddVertex(a1)
|
||||
g1.AddVertex(a2)
|
||||
g1.AddVertex(a3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:1", "s:2", "s:3"})
|
||||
g2.AddVertex(a123)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
140
engine/graph/autogroup/base.go
Normal file
140
engine/graph/autogroup/base.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
// baseGrouper is the base type for implementing the AutoGrouper interface.
|
||||
type baseGrouper struct {
|
||||
graph *pgraph.Graph // store a pointer to the graph
|
||||
vertices []pgraph.Vertex // cached list of vertices
|
||||
i int
|
||||
j int
|
||||
done bool
|
||||
}
|
||||
|
||||
// Name provides a friendly name for the logs to see.
|
||||
func (ag *baseGrouper) Name() string {
|
||||
return "baseGrouper"
|
||||
}
|
||||
|
||||
// Init is called only once and before using other AutoGrouper interface methods
|
||||
// the name method is the only exception: call it any time without side effects!
|
||||
func (ag *baseGrouper) Init(g *pgraph.Graph) error {
|
||||
if ag.graph != nil {
|
||||
return fmt.Errorf("the init method has already been called")
|
||||
}
|
||||
ag.graph = g // pointer
|
||||
ag.vertices = ag.graph.VerticesSorted() // cache in deterministic order!
|
||||
ag.i = 0
|
||||
ag.j = 0
|
||||
if len(ag.vertices) == 0 { // empty graph
|
||||
ag.done = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VertexNext is a simple iterator that loops through vertex (pair) combinations
|
||||
// an intelligent algorithm would selectively offer only valid pairs of vertices
|
||||
// these should satisfy logical grouping requirements for the autogroup designs!
|
||||
// the desired algorithms can override, but keep this method as a base iterator!
|
||||
func (ag *baseGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
|
||||
// this does a for v... { for w... { return v, w }} but stepwise!
|
||||
l := len(ag.vertices)
|
||||
if ag.i < l {
|
||||
v1 = ag.vertices[ag.i]
|
||||
}
|
||||
if ag.j < l {
|
||||
v2 = ag.vertices[ag.j]
|
||||
}
|
||||
|
||||
// in case the vertex was deleted
|
||||
if !ag.graph.HasVertex(v1) {
|
||||
v1 = nil
|
||||
}
|
||||
if !ag.graph.HasVertex(v2) {
|
||||
v2 = nil
|
||||
}
|
||||
|
||||
// two nested loops...
|
||||
if ag.j < l {
|
||||
ag.j++
|
||||
}
|
||||
if ag.j == l {
|
||||
ag.j = 0
|
||||
if ag.i < l {
|
||||
ag.i++
|
||||
}
|
||||
if ag.i == l {
|
||||
ag.done = true
|
||||
}
|
||||
}
|
||||
// TODO: is this index swap better or even valid?
|
||||
//if ag.i < l {
|
||||
// ag.i++
|
||||
//}
|
||||
//if ag.i == l {
|
||||
// ag.i = 0
|
||||
// if ag.j < l {
|
||||
// ag.j++
|
||||
// }
|
||||
// if ag.j == l {
|
||||
// ag.done = true
|
||||
// }
|
||||
//}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// VertexCmp can be used in addition to an overridding implementation.
|
||||
func (ag *baseGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
||||
if v1 == nil || v2 == nil {
|
||||
return fmt.Errorf("the vertex is nil")
|
||||
}
|
||||
if v1 == v2 { // skip yourself
|
||||
return fmt.Errorf("the vertices are the same")
|
||||
}
|
||||
|
||||
return nil // success
|
||||
}
|
||||
|
||||
// VertexMerge needs to be overridden to add the actual merging functionality.
|
||||
func (ag *baseGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
|
||||
return nil, fmt.Errorf("vertexMerge needs to be overridden")
|
||||
}
|
||||
|
||||
// EdgeMerge can be overridden, since it just simply returns the first edge.
|
||||
func (ag *baseGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
return e1 // noop
|
||||
}
|
||||
|
||||
// VertexTest processes the results of the grouping for the algorithm to know
|
||||
// return an error if something went horribly wrong, and bool false to stop.
|
||||
func (ag *baseGrouper) VertexTest(b bool) (bool, error) {
|
||||
// NOTE: this particular baseGrouper version doesn't track what happens
|
||||
// because since we iterate over every pair, we don't care which merge!
|
||||
if ag.done {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
72
engine/graph/autogroup/nonreachability.go
Normal file
72
engine/graph/autogroup/nonreachability.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// NonReachabilityGrouper is the most straight-forward algorithm for grouping.
|
||||
// TODO: this algorithm may not be correct in all cases. replace if needed!
|
||||
type NonReachabilityGrouper struct {
|
||||
baseGrouper // "inherit" what we want, and reimplement the rest
|
||||
}
|
||||
|
||||
// Name returns the name for the grouper algorithm.
|
||||
func (ag *NonReachabilityGrouper) Name() string {
|
||||
return "NonReachabilityGrouper"
|
||||
}
|
||||
|
||||
// VertexNext iteratively finds vertex pairs with simple graph reachability...
|
||||
// This algorithm relies on the observation that if there's a path from a to b,
|
||||
// then they *can't* be merged (b/c of the existing dependency) so therefore we
|
||||
// merge anything that *doesn't* satisfy this condition or that of the reverse!
|
||||
func (ag *NonReachabilityGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
|
||||
for {
|
||||
v1, v2, err = ag.baseGrouper.VertexNext() // get all iterable pairs
|
||||
if err != nil {
|
||||
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
|
||||
}
|
||||
|
||||
// ignore self cmp early (perf optimization)
|
||||
if v1 != v2 && v1 != nil && v2 != nil {
|
||||
// if NOT reachable, they're viable...
|
||||
out1, e1 := ag.graph.Reachability(v1, v2)
|
||||
if e1 != nil {
|
||||
return nil, nil, e1
|
||||
}
|
||||
out2, e2 := ag.graph.Reachability(v2, v1)
|
||||
if e2 != nil {
|
||||
return nil, nil, e2
|
||||
}
|
||||
if len(out1) == 0 && len(out2) == 0 {
|
||||
return // return v1 and v2, they're viable
|
||||
}
|
||||
}
|
||||
|
||||
// if we got here, it means we're skipping over this candidate!
|
||||
if ok, err := ag.baseGrouper.VertexTest(false); err != nil {
|
||||
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
|
||||
} else if !ok {
|
||||
return nil, nil, nil // done!
|
||||
}
|
||||
|
||||
// the vertexTest passed, so loop and try with a new pair...
|
||||
}
|
||||
}
|
||||
138
engine/graph/autogroup/util.go
Normal file
138
engine/graph/autogroup/util.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate, and
|
||||
// then by deleting v2 from the graph. Since more than one edge between two
|
||||
// vertices is not allowed, duplicate edges are merged as well. An edge merge
|
||||
// function can be provided if you'd like to control how you merge the edges!
|
||||
func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error), edgeMergeFn func(pgraph.Edge, pgraph.Edge) pgraph.Edge) error {
|
||||
// methodology
|
||||
// 1) edges between v1 and v2 are removed
|
||||
//Loop:
|
||||
for k1 := range g.Adjacency() {
|
||||
for k2 := range g.Adjacency()[k1] {
|
||||
// v1 -> v2 || v2 -> v1
|
||||
if (k1 == v1 && k2 == v2) || (k1 == v2 && k2 == v1) {
|
||||
delete(g.Adjacency()[k1], k2) // delete map & edge
|
||||
// NOTE: if we assume this is a DAG, then we can
|
||||
// assume only v1 -> v2 OR v2 -> v1 exists, and
|
||||
// we can break out of these loops immediately!
|
||||
//break Loop
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) edges that point towards v2 from X now point to v1 from X (no dupes)
|
||||
for _, x := range g.IncomingGraphVertices(v2) { // all to vertex v (??? -> v)
|
||||
e := g.Adjacency()[x][v2] // previous edge
|
||||
r, err := g.Reachability(x, v1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// merge e with ex := g.Adjacency()[x][v1] if it exists!
|
||||
if ex, exists := g.Adjacency()[x][v1]; exists && edgeMergeFn != nil && len(r) == 0 {
|
||||
e = edgeMergeFn(e, ex)
|
||||
}
|
||||
if len(r) == 0 { // if not reachable, add it
|
||||
g.AddEdge(x, v1, e) // overwrite edge
|
||||
} else if edgeMergeFn != nil { // reachable, merge e through...
|
||||
prev := x // initial condition
|
||||
for i, next := range r {
|
||||
if i == 0 {
|
||||
// next == prev, therefore skip
|
||||
continue
|
||||
}
|
||||
// this edge is from: prev, to: next
|
||||
ex, _ := g.Adjacency()[prev][next] // get
|
||||
ex = edgeMergeFn(ex, e)
|
||||
g.Adjacency()[prev][next] = ex // set
|
||||
prev = next
|
||||
}
|
||||
}
|
||||
delete(g.Adjacency()[x], v2) // delete old edge
|
||||
}
|
||||
|
||||
// 3) edges that point from v2 to X now point from v1 to X (no dupes)
|
||||
for _, x := range g.OutgoingGraphVertices(v2) { // all from vertex v (v -> ???)
|
||||
e := g.Adjacency()[v2][x] // previous edge
|
||||
r, err := g.Reachability(v1, x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// merge e with ex := g.Adjacency()[v1][x] if it exists!
|
||||
if ex, exists := g.Adjacency()[v1][x]; exists && edgeMergeFn != nil && len(r) == 0 {
|
||||
e = edgeMergeFn(e, ex)
|
||||
}
|
||||
if len(r) == 0 {
|
||||
g.AddEdge(v1, x, e) // overwrite edge
|
||||
} else if edgeMergeFn != nil { // reachable, merge e through...
|
||||
prev := v1 // initial condition
|
||||
for i, next := range r {
|
||||
if i == 0 {
|
||||
// next == prev, therefore skip
|
||||
continue
|
||||
}
|
||||
// this edge is from: prev, to: next
|
||||
ex, _ := g.Adjacency()[prev][next]
|
||||
ex = edgeMergeFn(ex, e)
|
||||
g.Adjacency()[prev][next] = ex
|
||||
prev = next
|
||||
}
|
||||
}
|
||||
delete(g.Adjacency()[v2], x)
|
||||
}
|
||||
|
||||
// 4) merge and then remove the (now merged/grouped) vertex
|
||||
if vertexMergeFn != nil { // run vertex merge function
|
||||
if v, err := vertexMergeFn(v1, v2); err != nil {
|
||||
return err
|
||||
} else if v != nil { // replace v1 with the "merged" version...
|
||||
// note: This branch isn't used if the vertexMergeFn
|
||||
// decides to just merge logically on its own instead
|
||||
// of actually returning something that we then merge.
|
||||
v1 = v // XXX: ineffassign?
|
||||
//*v1 = *v
|
||||
|
||||
// Ensure that everything still validates. (For safety!)
|
||||
r, ok := v1.(engine.Res) // TODO: v ?
|
||||
if !ok {
|
||||
return fmt.Errorf("not a Res")
|
||||
}
|
||||
if err := engine.Validate(r); err != nil {
|
||||
return errwrap.Wrapf(err, "the Res did not Validate")
|
||||
}
|
||||
}
|
||||
}
|
||||
g.DeleteVertex(v2) // remove grouped vertex
|
||||
|
||||
// 5) creation of a cyclic graph should throw an error
|
||||
if _, err := g.TopologicalSort(); err != nil { // am i a dag or not?
|
||||
return errwrap.Wrapf(err, "the TopologicalSort failed") // not a dag
|
||||
}
|
||||
return nil // success
|
||||
}
|
||||
430
engine/graph/engine.go
Normal file
430
engine/graph/engine.go
Normal file
@@ -0,0 +1,430 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
|
||||
"github.com/purpleidea/mgmt/converger"
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
"github.com/purpleidea/mgmt/util/semaphore"
|
||||
)
|
||||
|
||||
const (
|
||||
// StateDir is the name of the sub directory where all the local
|
||||
// resource state is stored.
|
||||
StateDir = "state"
|
||||
)
|
||||
|
||||
// Engine encapsulates a generic graph and manages its operations.
|
||||
type Engine struct {
|
||||
Program string
|
||||
Hostname string
|
||||
World engine.World
|
||||
|
||||
// Prefix is a unique directory prefix which can be used. It should be
|
||||
// created if needed.
|
||||
Prefix string
|
||||
Converger *converger.Coordinator
|
||||
|
||||
Debug bool
|
||||
Logf func(format string, v ...interface{})
|
||||
|
||||
graph *pgraph.Graph
|
||||
nextGraph *pgraph.Graph
|
||||
state map[pgraph.Vertex]*State
|
||||
waits map[pgraph.Vertex]*sync.WaitGroup // wg for the Worker func
|
||||
wlock *sync.Mutex // lock around waits map
|
||||
|
||||
slock *sync.Mutex // semaphore lock
|
||||
semas map[string]*semaphore.Semaphore
|
||||
|
||||
wg *sync.WaitGroup // wg for the whole engine (only used for close)
|
||||
|
||||
paused bool // are we paused?
|
||||
fastPause bool
|
||||
}
|
||||
|
||||
// Init initializes the internal structures and starts this the graph running.
|
||||
// If the struct does not validate, or it cannot initialize, then this errors.
|
||||
// Initially it will contain an empty graph.
|
||||
func (obj *Engine) Init() error {
|
||||
if obj.Program == "" {
|
||||
return fmt.Errorf("the Program is empty")
|
||||
}
|
||||
if obj.Hostname == "" {
|
||||
return fmt.Errorf("the Hostname is empty")
|
||||
}
|
||||
|
||||
var err error
|
||||
if obj.graph, err = pgraph.NewGraph("graph"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if obj.Prefix == "" || obj.Prefix == "/" {
|
||||
return fmt.Errorf("the prefix of `%s` is invalid", obj.Prefix)
|
||||
}
|
||||
if err := os.MkdirAll(obj.Prefix, 0770); err != nil {
|
||||
return errwrap.Wrapf(err, "can't create prefix")
|
||||
}
|
||||
|
||||
obj.state = make(map[pgraph.Vertex]*State)
|
||||
obj.waits = make(map[pgraph.Vertex]*sync.WaitGroup)
|
||||
obj.wlock = &sync.Mutex{}
|
||||
|
||||
obj.slock = &sync.Mutex{}
|
||||
obj.semas = make(map[string]*semaphore.Semaphore)
|
||||
|
||||
obj.wg = &sync.WaitGroup{}
|
||||
|
||||
obj.paused = true // start off true, so we can Resume after first Commit
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load a new graph into the engine. Offline graph operations will be performed
|
||||
// on this graph. To switch it to the active graph, and run it, use Commit.
|
||||
func (obj *Engine) Load(newGraph *pgraph.Graph) error {
|
||||
if obj.nextGraph != nil {
|
||||
return fmt.Errorf("can't overwrite pending graph, use abort")
|
||||
}
|
||||
obj.nextGraph = newGraph
|
||||
return nil
|
||||
}
|
||||
|
||||
// Abort the pending graph and any work in progress on it. After this call you
|
||||
// may Load a new graph.
|
||||
func (obj *Engine) Abort() error {
|
||||
if obj.nextGraph == nil {
|
||||
return fmt.Errorf("there is no pending graph to abort")
|
||||
}
|
||||
obj.nextGraph = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate validates the pending graph to ensure it is appropriate for the
|
||||
// engine. This should be called before Commit to avoid any surprises there!
|
||||
// This prevents an error on Commit which could cause an engine shutdown.
|
||||
func (obj *Engine) Validate() error {
|
||||
for _, vertex := range obj.nextGraph.Vertices() {
|
||||
res, ok := vertex.(engine.Res)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a Res")
|
||||
}
|
||||
|
||||
if err := engine.Validate(res); err != nil {
|
||||
return errwrap.Wrapf(err, "the Res did not Validate")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Apply a function to the pending graph. You must pass in a function which will
|
||||
// receive this graph as input, and return an error if something does not
|
||||
// succeed.
|
||||
func (obj *Engine) Apply(fn func(*pgraph.Graph) error) error {
|
||||
return fn(obj.nextGraph)
|
||||
}
|
||||
|
||||
// Commit runs a graph sync and swaps the loaded graph with the current one. If
|
||||
// it errors, then the running graph wasn't changed. It is recommended that you
|
||||
// pause the engine before running this, and resume it after you're done.
|
||||
func (obj *Engine) Commit() error {
|
||||
// TODO: Does this hurt performance or graph changes ?
|
||||
|
||||
start := []func() error{} // functions to run after graphsync to start...
|
||||
vertexAddFn := func(vertex pgraph.Vertex) error {
|
||||
// some of these validation steps happen before this Commit step
|
||||
// in Validate() to avoid erroring here. These are redundant.
|
||||
// FIXME: should we get rid of this redundant validation?
|
||||
res, ok := vertex.(engine.Res)
|
||||
if !ok { // should not happen, previously validated
|
||||
return fmt.Errorf("not a Res")
|
||||
}
|
||||
if obj.Debug {
|
||||
obj.Logf("loading resource `%s`", res)
|
||||
}
|
||||
|
||||
if _, exists := obj.state[vertex]; exists {
|
||||
return fmt.Errorf("the Res state already exists")
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("Validate(%s)", res)
|
||||
}
|
||||
err := engine.Validate(res)
|
||||
if obj.Debug {
|
||||
obj.Logf("Validate(%s): Return(%+v)", res, err)
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the Res did not Validate")
|
||||
}
|
||||
|
||||
pathUID := engineUtil.ResPathUID(res)
|
||||
statePrefix := fmt.Sprintf("%s/", path.Join(obj.statePrefix(), pathUID))
|
||||
|
||||
// don't create this unless it *will* be used
|
||||
//if err := os.MkdirAll(statePrefix, 0770); err != nil {
|
||||
// return errwrap.Wrapf(err, "can't create state prefix")
|
||||
//}
|
||||
|
||||
obj.waits[vertex] = &sync.WaitGroup{}
|
||||
obj.state[vertex] = &State{
|
||||
Graph: obj.graph, // Update if we swap the graph!
|
||||
Vertex: vertex,
|
||||
|
||||
Program: obj.Program,
|
||||
Hostname: obj.Hostname,
|
||||
|
||||
World: obj.World,
|
||||
Prefix: statePrefix,
|
||||
//Converger: obj.Converger,
|
||||
|
||||
Debug: obj.Debug,
|
||||
Logf: func(format string, v ...interface{}) {
|
||||
obj.Logf(res.String()+": "+format, v...)
|
||||
},
|
||||
}
|
||||
if err := obj.state[vertex].Init(); err != nil {
|
||||
return errwrap.Wrapf(err, "the Res did not Init")
|
||||
}
|
||||
|
||||
fn := func() error {
|
||||
// start the Worker
|
||||
obj.wg.Add(1)
|
||||
obj.wlock.Lock()
|
||||
obj.waits[vertex].Add(1)
|
||||
obj.wlock.Unlock()
|
||||
go func(v pgraph.Vertex) {
|
||||
defer obj.wg.Done()
|
||||
defer func() {
|
||||
// we need this lock, because this go
|
||||
// routine could run when the next fn
|
||||
// function above here is running...
|
||||
obj.wlock.Lock()
|
||||
obj.waits[v].Done()
|
||||
obj.wlock.Unlock()
|
||||
}()
|
||||
|
||||
obj.Logf("Worker(%s)", v)
|
||||
// contains the Watch and CheckApply loops
|
||||
err := obj.Worker(v)
|
||||
obj.Logf("Worker(%s): Exited(%+v)", v, err)
|
||||
obj.state[v].workerErr = err // store the error
|
||||
// If the Rewatch metaparam is true, then this will get
|
||||
// restarted if we do a graph cmp swap. This is why the
|
||||
// graph cmp function runs the removes before the adds.
|
||||
// XXX: This should feed into an $error var in the lang.
|
||||
}(vertex)
|
||||
return nil
|
||||
}
|
||||
start = append(start, fn) // do this at the end, if it's needed
|
||||
return nil
|
||||
}
|
||||
|
||||
free := []func() error{} // functions to run after graphsync to reset...
|
||||
vertexRemoveFn := func(vertex pgraph.Vertex) error {
|
||||
// wait for exit before starting new graph!
|
||||
close(obj.state[vertex].removeDone) // causes doneChan to close
|
||||
obj.state[vertex].Resume() // unblock from resume
|
||||
obj.waits[vertex].Wait() // sync
|
||||
|
||||
// close the state and resource
|
||||
// FIXME: will this mess up the sync and block the engine?
|
||||
if err := obj.state[vertex].Close(); err != nil {
|
||||
return errwrap.Wrapf(err, "the Res did not Close")
|
||||
}
|
||||
|
||||
// delete to free up memory from old graphs
|
||||
fn := func() error {
|
||||
delete(obj.state, vertex)
|
||||
delete(obj.waits, vertex)
|
||||
return nil
|
||||
}
|
||||
free = append(free, fn) // do this at the end, so we don't panic
|
||||
return nil
|
||||
}
|
||||
|
||||
// add the Worker swap (reload) on error decision into this vertexCmpFn
|
||||
vertexCmpFn := func(v1, v2 pgraph.Vertex) (bool, error) {
|
||||
r1, ok1 := v1.(engine.Res)
|
||||
r2, ok2 := v2.(engine.Res)
|
||||
if !ok1 || !ok2 { // should not happen, previously validated
|
||||
return false, fmt.Errorf("not a Res")
|
||||
}
|
||||
m1 := r1.MetaParams()
|
||||
m2 := r2.MetaParams()
|
||||
swap1, swap2 := true, true // assume default of true
|
||||
if m1 != nil {
|
||||
swap1 = m1.Rewatch
|
||||
}
|
||||
if m2 != nil {
|
||||
swap2 = m2.Rewatch
|
||||
}
|
||||
|
||||
s1, ok1 := obj.state[v1]
|
||||
s2, ok2 := obj.state[v2]
|
||||
x1, x2 := false, false
|
||||
if ok1 {
|
||||
x1 = s1.workerErr != nil && swap1
|
||||
}
|
||||
if ok2 {
|
||||
x2 = s2.workerErr != nil && swap2
|
||||
}
|
||||
|
||||
if x1 || x2 {
|
||||
// We swap, even if they're the same, so that we reload!
|
||||
// This causes an add and remove of the "same" vertex...
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return engine.VertexCmpFn(v1, v2) // do the normal cmp otherwise
|
||||
}
|
||||
|
||||
// If GraphSync succeeds, it updates the receiver graph accordingly...
|
||||
// Running the shutdown in vertexRemoveFn does not need to happen in a
|
||||
// topologically sorted order because it already paused in that order.
|
||||
obj.Logf("graph sync...")
|
||||
if err := obj.graph.GraphSync(obj.nextGraph, vertexCmpFn, vertexAddFn, vertexRemoveFn, engine.EdgeCmpFn); err != nil {
|
||||
return errwrap.Wrapf(err, "error running graph sync")
|
||||
}
|
||||
// We run these afterwards, so that we don't unnecessarily start anyone
|
||||
// if GraphSync failed in some way. Otherwise we'd have to do clean up!
|
||||
for _, fn := range start {
|
||||
if err := fn(); err != nil {
|
||||
return errwrap.Wrapf(err, "error running start fn")
|
||||
}
|
||||
}
|
||||
// We run these afterwards, so that the state structs (that might get
|
||||
// referenced) are not destroyed while someone might poke or use one.
|
||||
for _, fn := range free {
|
||||
if err := fn(); err != nil {
|
||||
return errwrap.Wrapf(err, "error running free fn")
|
||||
}
|
||||
}
|
||||
obj.nextGraph = nil
|
||||
|
||||
// After this point, we must not error or we'd need to restore all of
|
||||
// the changes that we'd made to the previously primary graph. This is
|
||||
// because this function is meant to atomically swap the graphs safely.
|
||||
|
||||
// Update all the `State` structs with the new Graph pointer.
|
||||
for _, vertex := range obj.graph.Vertices() {
|
||||
state, exists := obj.state[vertex]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
state.Graph = obj.graph // update pointer to graph
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume runs the currently active graph. It also un-pauses the graph if it was
|
||||
// paused. Very little that is interesting should happen here. It all happens in
|
||||
// the Commit method. After Commit, new things are already started, but we still
|
||||
// need to Resume any pre-existing resources.
|
||||
func (obj *Engine) Resume() error {
|
||||
if !obj.paused {
|
||||
return fmt.Errorf("already resumed")
|
||||
}
|
||||
|
||||
topoSort, err := obj.graph.TopologicalSort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//indegree := obj.graph.InDegree() // compute all of the indegree's
|
||||
reversed := pgraph.Reverse(topoSort)
|
||||
|
||||
for _, vertex := range reversed {
|
||||
//obj.state[vertex].starter = (indegree[vertex] == 0)
|
||||
obj.state[vertex].Resume() // doesn't error
|
||||
}
|
||||
// we wait for everyone to start before exiting!
|
||||
obj.paused = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFastPause puts the graph into fast pause mode. This is usually done via
|
||||
// the argument to the Pause command, but this method can be used if a pause was
|
||||
// already started, and you'd like subsequent parts to pause quickly. Once in
|
||||
// fast pause mode for a given pause action, you cannot switch to regular pause.
|
||||
// This is because once you've started a fast pause, some dependencies might
|
||||
// have been skipped when fast pausing, and future resources might have missed a
|
||||
// poke. In general this is only called when you're trying to hurry up the exit.
|
||||
// XXX: Not implemented
|
||||
func (obj *Engine) SetFastPause() {
|
||||
obj.fastPause = true
|
||||
}
|
||||
|
||||
// Pause the active, running graph.
|
||||
func (obj *Engine) Pause(fastPause bool) error {
|
||||
if obj.paused {
|
||||
return fmt.Errorf("already paused")
|
||||
}
|
||||
|
||||
obj.fastPause = fastPause
|
||||
topoSort, _ := obj.graph.TopologicalSort()
|
||||
for _, vertex := range topoSort { // squeeze out the events...
|
||||
// The Event is sent to an unbuffered channel, so this event is
|
||||
// synchronous, and as a result it blocks until it is received.
|
||||
if err := obj.state[vertex].Pause(); err != nil && err != engine.ErrClosed {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
obj.paused = true
|
||||
|
||||
// we are now completely paused...
|
||||
obj.fastPause = false // reset
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close triggers a shutdown. Engine must be already paused before this is run.
|
||||
func (obj *Engine) Close() error {
|
||||
emptyGraph, reterr := pgraph.NewGraph("empty")
|
||||
|
||||
// this is a graph switch (graph sync) that switches to an empty graph!
|
||||
if err := obj.Load(emptyGraph); err != nil { // copy in empty graph
|
||||
reterr = errwrap.Append(reterr, err)
|
||||
}
|
||||
// FIXME: Do we want to run commit if Load failed? Does this even work?
|
||||
// the commit will cause the graph sync to shut things down cleverly...
|
||||
if err := obj.Commit(); err != nil {
|
||||
reterr = errwrap.Append(reterr, err)
|
||||
}
|
||||
|
||||
obj.wg.Wait() // for now, this doesn't need to be a separate Wait() method
|
||||
return reterr
|
||||
}
|
||||
|
||||
// Graph returns the running graph.
|
||||
func (obj *Engine) Graph() *pgraph.Graph {
|
||||
return obj.graph
|
||||
}
|
||||
|
||||
// statePrefix returns the dir where all the resource state is stored locally.
|
||||
func (obj *Engine) statePrefix() string {
|
||||
return fmt.Sprintf("%s/", path.Join(obj.Prefix, StateDir))
|
||||
}
|
||||
37
engine/graph/graph_test.go
Normal file
37
engine/graph/graph_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func TestMultiErr(t *testing.T) {
|
||||
var err error
|
||||
e := fmt.Errorf("some error")
|
||||
err = errwrap.Append(err, e) // build an error from a nil base
|
||||
// ensure that this lib allows us to append to a nil
|
||||
if err == nil {
|
||||
t.Errorf("missing error")
|
||||
}
|
||||
}
|
||||
59
engine/graph/refresh.go
Normal file
59
engine/graph/refresh.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
// RefreshPending determines if any previous nodes have a refresh pending here.
|
||||
// If this is true, it means I am expected to apply a refresh when I next run.
|
||||
func (obj *Engine) RefreshPending(vertex pgraph.Vertex) bool {
|
||||
var refresh bool
|
||||
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
|
||||
// if we asked for a notify *and* if one is pending!
|
||||
edge := e.(*engine.Edge) // panic if wrong
|
||||
if edge.Notify && edge.Refresh() {
|
||||
refresh = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return refresh
|
||||
}
|
||||
|
||||
// SetUpstreamRefresh sets the refresh value to any upstream vertices.
|
||||
func (obj *Engine) SetUpstreamRefresh(vertex pgraph.Vertex, b bool) {
|
||||
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
|
||||
edge := e.(*engine.Edge) // panic if wrong
|
||||
if edge.Notify {
|
||||
edge.SetRefresh(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetDownstreamRefresh sets the refresh value to any downstream vertices.
|
||||
func (obj *Engine) SetDownstreamRefresh(vertex pgraph.Vertex, b bool) {
|
||||
for _, e := range obj.graph.OutgoingGraphEdges(vertex) {
|
||||
edge := e.(*engine.Edge) // panic if wrong
|
||||
// if we asked for a notify *and* if one is pending!
|
||||
if edge.Notify {
|
||||
edge.SetRefresh(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
300
engine/graph/reverse.go
Normal file
300
engine/graph/reverse.go
Normal file
@@ -0,0 +1,300 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
const (
|
||||
// ReverseFile is the file name in the resource state dir where any
|
||||
// reversal information is stored.
|
||||
ReverseFile = "reverse"
|
||||
|
||||
// ReversePerm is the permissions mode used to create the ReverseFile.
|
||||
ReversePerm = 0600
|
||||
)
|
||||
|
||||
// Reversals adds the reversals onto the loaded graph. This should happen last,
|
||||
// and before Commit.
|
||||
func (obj *Engine) Reversals() error {
|
||||
if obj.nextGraph == nil {
|
||||
return fmt.Errorf("there is no active graph to add reversals to")
|
||||
}
|
||||
|
||||
// Initially get all of the reversals to seek out all possible errors.
|
||||
// XXX: The engine needs to know where data might have been stored if we
|
||||
// XXX: want to potentially allow alternate read/write paths, like etcd.
|
||||
// XXX: In this scenario, we'd have to store a token somewhere to let us
|
||||
// XXX: know to look elsewhere for the special ReversalList read method.
|
||||
data, err := obj.ReversalList() // (map[string]string, error)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the reversals had errors")
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil // end early
|
||||
}
|
||||
|
||||
resMatch := func(r1, r2 engine.Res) bool { // simple match on UID only!
|
||||
if r1.Kind() != r2.Kind() {
|
||||
return false
|
||||
}
|
||||
if r1.Name() != r2.Name() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
resInList := func(needle engine.Res, haystack []engine.Res) bool {
|
||||
for _, res := range haystack {
|
||||
if resMatch(needle, res) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("decoding %d reversals...", len(data))
|
||||
}
|
||||
resources := []engine.Res{}
|
||||
|
||||
// do this in a sorted order so that it errors deterministically
|
||||
sorted := []string{}
|
||||
for key := range data {
|
||||
sorted = append(sorted, key)
|
||||
}
|
||||
sort.Strings(sorted)
|
||||
for _, key := range sorted {
|
||||
val := data[key]
|
||||
// XXX: replace this ResToB64 method with one that stores it in
|
||||
// a human readable format, in case someone wants to hack and
|
||||
// edit it manually.
|
||||
// XXX: we probably want this to be YAML, it works with the diff
|
||||
// too...
|
||||
r, err := engineUtil.B64ToRes(val)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error decoding res with UID: `%s`", key)
|
||||
}
|
||||
|
||||
res, ok := r.(engine.ReversibleRes)
|
||||
if !ok {
|
||||
// this requirement is here to keep things simpler...
|
||||
return errwrap.Wrapf(err, "decoded res with UID: `%s` was not reversible", key)
|
||||
}
|
||||
|
||||
matchFn := func(vertex pgraph.Vertex) (bool, error) {
|
||||
r, ok := vertex.(engine.Res)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("not a Res")
|
||||
}
|
||||
if !resMatch(r, res) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// FIXME: not efficient, we could build a cache-map first
|
||||
vertex, err := obj.nextGraph.VertexMatchFn(matchFn) // (Vertex, error)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error searching graph for match")
|
||||
}
|
||||
if vertex != nil { // found one!
|
||||
continue // it doesn't need reversing yet
|
||||
}
|
||||
|
||||
// TODO: check for (incompatible?) duplicates instead
|
||||
if resInList(res, resources) { // we've already got this one...
|
||||
continue
|
||||
}
|
||||
|
||||
// We set this in two different places to be safe. It ensures
|
||||
// that we erase the reversal state file after we've used it.
|
||||
res.ReversibleMeta().Reversal = true // set this for later...
|
||||
|
||||
resources = append(resources, res)
|
||||
}
|
||||
|
||||
if len(resources) == 0 {
|
||||
return nil // end early
|
||||
}
|
||||
|
||||
// Now that we've passed the chance of any errors, we modify the graph.
|
||||
obj.Logf("adding %d reversals...", len(resources))
|
||||
for _, res := range resources {
|
||||
obj.nextGraph.AddVertex(res)
|
||||
}
|
||||
// TODO: Do we want a way for stored reversals to add edges too?
|
||||
|
||||
// It would be great to ensure we didn't add any graph cycles here, but
|
||||
// instead of checking now, we'll move the check into the main loop.
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReversalList returns all the available pending reversal data on this host. It
|
||||
// can then be decoded by whatever method is appropriate for.
|
||||
func (obj *Engine) ReversalList() (map[string]string, error) {
|
||||
result := make(map[string]string) // some key to contents
|
||||
|
||||
dir := obj.statePrefix() // loop through this dir...
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, errwrap.Wrapf(err, "error reading list of state dirs")
|
||||
} else if err != nil {
|
||||
return result, nil // nothing found, no state dir exists yet
|
||||
}
|
||||
|
||||
for _, x := range files {
|
||||
key := x.Name() // some uid for the resource
|
||||
file := path.Join(dir, key, ReverseFile)
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, errwrap.Wrapf(err, "could not read reverse file: %s", file)
|
||||
} else if err != nil {
|
||||
continue // file does not exist, skip
|
||||
}
|
||||
|
||||
// file exists!
|
||||
str := string(content)
|
||||
result[key] = str // save
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReversalInit performs the reversal initialization steps if necessary for this
|
||||
// resource.
|
||||
func (obj *State) ReversalInit() error {
|
||||
res, ok := obj.Vertex.(engine.ReversibleRes)
|
||||
if !ok {
|
||||
return nil // nothing to do
|
||||
}
|
||||
|
||||
if res.ReversibleMeta().Disabled {
|
||||
return nil // nothing to do, reversal isn't enabled
|
||||
}
|
||||
|
||||
// If the reversal is enabled, but we are the result of a previous
|
||||
// reversal, then this will overwrite that older reversal request, and
|
||||
// our resource should be designed to deal with that. This happens if we
|
||||
// return a reversible resource as the reverse of a resource that was
|
||||
// reversed. It's probably fairly rare.
|
||||
if res.ReversibleMeta().Reversal {
|
||||
obj.Logf("triangle reversal") // warn!
|
||||
}
|
||||
|
||||
r, err := res.Reversed()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not reverse: %s", res.String())
|
||||
}
|
||||
if r == nil {
|
||||
return nil // this can't be reversed, or isn't implemented here
|
||||
}
|
||||
|
||||
// We set this in two different places to be safe. It ensures that we
|
||||
// erase the reversal state file after we've used it.
|
||||
r.ReversibleMeta().Reversal = true // set this for later...
|
||||
|
||||
// XXX: replace this ResToB64 method with one that stores it in a human
|
||||
// readable format, in case someone wants to hack and edit it manually.
|
||||
// XXX: we probably want this to be YAML, it works with the diff too...
|
||||
str, err := engineUtil.ResToB64(r)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not encode: %s", res.String())
|
||||
}
|
||||
|
||||
// TODO: put this method on traits.Reversible as part of the interface?
|
||||
return obj.ReversalWrite(str, res.ReversibleMeta().Overwrite) // Store!
|
||||
}
|
||||
|
||||
// ReversalClose performs the reversal shutdown steps if necessary for this
|
||||
// resource.
|
||||
func (obj *State) ReversalClose() error {
|
||||
res, ok := obj.Vertex.(engine.ReversibleRes)
|
||||
if !ok {
|
||||
return nil // nothing to do
|
||||
}
|
||||
|
||||
// Don't check res.ReversibleMeta().Disabled because we're removing the
|
||||
// previous one. That value only applies if we're doing a new reversal.
|
||||
|
||||
if !res.ReversibleMeta().Reversal {
|
||||
return nil // nothing to erase, we're not a reversal resource
|
||||
}
|
||||
|
||||
if !obj.isStateOK { // did we successfully reverse?
|
||||
obj.Logf("did not complete reversal") // warn
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: put this method on traits.Reversible as part of the interface?
|
||||
return obj.ReversalDelete() // Erase our reversal instructions.
|
||||
}
|
||||
|
||||
// ReversalWrite stores the reversal state information for this resource.
|
||||
func (obj *State) ReversalWrite(str string, overwrite bool) error {
|
||||
dir, err := obj.varDir("") // private version
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not get VarDir for reverse")
|
||||
}
|
||||
file := path.Join(dir, ReverseFile) // return a unique file
|
||||
|
||||
content, err := ioutil.ReadFile(file)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return errwrap.Wrapf(err, "could not read reverse file: %s", file)
|
||||
}
|
||||
|
||||
// file exists and we shouldn't overwrite if different
|
||||
if err == nil && !overwrite {
|
||||
// compare to existing file
|
||||
oldStr := string(content)
|
||||
if str != oldStr {
|
||||
obj.Logf("existing, pending, reversible resource exists")
|
||||
//obj.Logf("diff:")
|
||||
//obj.Logf("") // TODO: print the diff w/o and secret values
|
||||
return fmt.Errorf("existing, pending, reversible resource exists")
|
||||
}
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(file, []byte(str), ReversePerm)
|
||||
}
|
||||
|
||||
// ReversalDelete removes the reversal state information for this resource.
|
||||
func (obj *State) ReversalDelete() error {
|
||||
dir, err := obj.varDir("") // private version
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not get VarDir for reverse")
|
||||
}
|
||||
file := path.Join(dir, ReverseFile) // return a unique file
|
||||
|
||||
// FIXME: why do we see these removals when there isn't a state file?
|
||||
if err = os.Remove(file); os.IsNotExist(err) {
|
||||
return nil // ignore missing files
|
||||
}
|
||||
|
||||
return errwrap.Wrapf(err, "could not remove reverse state file")
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package resources
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -23,16 +23,15 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
"github.com/purpleidea/mgmt/util/semaphore"
|
||||
|
||||
multierr "github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
// SemaSep is the trailing separator to split the semaphore id from the size.
|
||||
const SemaSep = ":"
|
||||
|
||||
// SemaLock acquires the list of semaphores in the graph.
|
||||
func (obj *MGraph) SemaLock(semas []string) error {
|
||||
// semaLock acquires the list of semaphores in the graph.
|
||||
func (obj *Engine) semaLock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // very important to avoid deadlock in the dag!
|
||||
|
||||
@@ -46,15 +45,14 @@ func (obj *MGraph) SemaLock(semas []string) error {
|
||||
}
|
||||
obj.slock.Unlock()
|
||||
|
||||
if err := sema.P(1); err != nil { // lock!
|
||||
reterr = multierr.Append(reterr, err) // list of errors
|
||||
}
|
||||
err := sema.P(1) // lock!
|
||||
reterr = errwrap.Append(reterr, err) // list of errors
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// SemaUnlock releases the list of semaphores in the graph.
|
||||
func (obj *MGraph) SemaUnlock(semas []string) error {
|
||||
// semaUnlock releases the list of semaphores in the graph.
|
||||
func (obj *Engine) semaUnlock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // unlock in the same order to remove partial locks
|
||||
|
||||
@@ -65,9 +63,8 @@ func (obj *MGraph) SemaUnlock(semas []string) error {
|
||||
panic(fmt.Sprintf("graph: sema: %s does not exist", id))
|
||||
}
|
||||
|
||||
if err := sema.V(1); err != nil { // unlock!
|
||||
reterr = multierr.Append(reterr, err) // list of errors
|
||||
}
|
||||
err := sema.V(1) // unlock!
|
||||
reterr = errwrap.Append(reterr, err) // list of errors
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
37
engine/graph/semaphore_test.go
Normal file
37
engine/graph/semaphore_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSemaSize(t *testing.T) {
|
||||
pairs := map[string]int{
|
||||
"id:42": 42,
|
||||
":13": 13,
|
||||
"some_id": 1,
|
||||
}
|
||||
for id, size := range pairs {
|
||||
if i := SemaSize(id); i != size {
|
||||
t.Errorf("sema id `%s`, expected: `%d`, got: `%d`", id, size, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
152
engine/graph/sendrecv.go
Normal file
152
engine/graph/sendrecv.go
Normal file
@@ -0,0 +1,152 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// SendRecv pulls in the sent values into the receive slots. It is called by the
|
||||
// receiver and must be given as input the full resource struct to receive on.
|
||||
// It applies the loaded values to the resource.
|
||||
func (obj *Engine) SendRecv(res engine.RecvableRes) (map[string]bool, error) {
|
||||
recv := res.Recv()
|
||||
if obj.Debug {
|
||||
// NOTE: this could expose private resource data like passwords
|
||||
obj.Logf("%s: SendRecv: %+v", res, recv)
|
||||
}
|
||||
var updated = make(map[string]bool) // list of updated keys
|
||||
var err error
|
||||
for k, v := range recv {
|
||||
updated[k] = false // default
|
||||
v.Changed = false // reset to the default
|
||||
|
||||
var st interface{} = v.Res // old style direct send/recv
|
||||
if true { // new style send/recv API
|
||||
st = v.Res.Sent()
|
||||
}
|
||||
|
||||
if st == nil {
|
||||
e := fmt.Errorf("received nil value from: %s", v.Res)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
if e := engineUtil.StructFieldCompat(st, v.Key, res, k); e != nil {
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
// send
|
||||
m1, e := engineUtil.StructTagToFieldName(st)
|
||||
if e != nil {
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
key1, exists := m1[v.Key]
|
||||
if !exists {
|
||||
e := fmt.Errorf("requested key of `%s` not found in send struct", v.Key)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
obj1 := reflect.Indirect(reflect.ValueOf(st))
|
||||
type1 := obj1.Type()
|
||||
value1 := obj1.FieldByName(key1)
|
||||
kind1 := value1.Kind()
|
||||
|
||||
// recv
|
||||
m2, e := engineUtil.StructTagToFieldName(res)
|
||||
if e != nil {
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
key2, exists := m2[k]
|
||||
if !exists {
|
||||
e := fmt.Errorf("requested key of `%s` not found in recv struct", k)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
obj2 := reflect.Indirect(reflect.ValueOf(res)) // pass in full struct
|
||||
type2 := obj2.Type()
|
||||
value2 := obj2.FieldByName(key2)
|
||||
kind2 := value2.Kind()
|
||||
|
||||
if obj.Debug {
|
||||
obj.Logf("Send(%s) has %v: %v", type1, kind1, value1)
|
||||
obj.Logf("Recv(%s) has %v: %v", type2, kind2, value2)
|
||||
}
|
||||
|
||||
// i think we probably want the same kind, at least for now...
|
||||
if kind1 != kind2 {
|
||||
e := fmt.Errorf("kind mismatch between %s: %s and %s: %s", v.Res, kind1, res, kind2)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
// if the types don't match, we can't use send->recv
|
||||
// FIXME: do we want to relax this for string -> *string ?
|
||||
if e := TypeCmp(value1, value2); e != nil {
|
||||
e := errwrap.Wrapf(e, "type mismatch between %s and %s", v.Res, res)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
// if we can't set, then well this is pointless!
|
||||
if !value2.CanSet() {
|
||||
e := fmt.Errorf("can't set %s.%s", res, k)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
// if we can't interface, we can't compare...
|
||||
if !value1.CanInterface() || !value2.CanInterface() {
|
||||
e := fmt.Errorf("can't interface %s.%s", res, k)
|
||||
err = errwrap.Append(err, e) // list of errors
|
||||
continue
|
||||
}
|
||||
|
||||
// if the values aren't equal, we're changing the receiver
|
||||
if !reflect.DeepEqual(value1.Interface(), value2.Interface()) {
|
||||
// TODO: can we catch the panics here in case they happen?
|
||||
value2.Set(value1) // do it for all types that match
|
||||
updated[k] = true // we updated this key!
|
||||
v.Changed = true // tag this key as updated!
|
||||
obj.Logf("SendRecv: %s.%s -> %s.%s", v.Res, v.Key, res, k)
|
||||
}
|
||||
}
|
||||
return updated, err
|
||||
}
|
||||
|
||||
// TypeCmp compares two reflect values to see if they are the same Kind. It can
|
||||
// look into a ptr Kind to see if the underlying pair of ptr's can TypeCmp too!
|
||||
func TypeCmp(a, b reflect.Value) error {
|
||||
ta, tb := a.Type(), b.Type()
|
||||
if ta != tb {
|
||||
return fmt.Errorf("type mismatch: %s != %s", ta, tb)
|
||||
}
|
||||
// NOTE: it seems we don't need to recurse into pointers to sub check!
|
||||
|
||||
return nil // identical Type()'s
|
||||
}
|
||||
408
engine/graph/state.go
Normal file
408
engine/graph/state.go
Normal file
@@ -0,0 +1,408 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/converger"
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// State stores some state about the resource it is mapped to.
|
||||
type State struct {
|
||||
// Graph is a pointer to the graph that this vertex is part of.
|
||||
Graph *pgraph.Graph
|
||||
|
||||
// Vertex is the pointer in the graph that this state corresponds to. It
|
||||
// can be converted to a `Res` if necessary.
|
||||
// TODO: should this be passed in on Init instead?
|
||||
Vertex pgraph.Vertex
|
||||
|
||||
Program string
|
||||
Hostname string
|
||||
World engine.World
|
||||
|
||||
// Prefix is a unique directory prefix which can be used. It should be
|
||||
// created if needed.
|
||||
Prefix string
|
||||
|
||||
//Converger *converger.Coordinator
|
||||
|
||||
// Debug turns on additional output and behaviours.
|
||||
Debug bool
|
||||
|
||||
// Logf is the logging function that should be used to display messages.
|
||||
Logf func(format string, v ...interface{})
|
||||
|
||||
timestamp int64 // last updated timestamp
|
||||
isStateOK bool // is state OK or do we need to run CheckApply ?
|
||||
workerErr error // did the Worker error?
|
||||
|
||||
// doneChan closes when Watch should shut down. When any of the
|
||||
// following channels close, it causes this to close.
|
||||
doneChan chan struct{}
|
||||
|
||||
// processDone is closed when the Process/CheckApply function fails
|
||||
// permanently, and wants to cause Watch to exit.
|
||||
processDone chan struct{}
|
||||
// watchDone is closed when the Watch function fails permanently, and we
|
||||
// close this to signal we should definitely exit. (Often redundant.)
|
||||
watchDone chan struct{} // could be shared with limitDone
|
||||
// limitDone is closed when the Watch function fails permanently, and we
|
||||
// close this to signal we should definitely exit. This happens inside
|
||||
// of the limit loop of the Process section of Worker.
|
||||
limitDone chan struct{} // could be shared with watchDone
|
||||
// removeDone is closed when the vertexRemoveFn method asks for an exit.
|
||||
// This happens when we're switching graphs. The switch to an "empty" is
|
||||
// the equivalent of asking for a final shutdown.
|
||||
removeDone chan struct{}
|
||||
// eventsDone is closed when we shutdown the Process loop because we
|
||||
// closed without error. In theory this shouldn't happen, but it could
|
||||
// if Watch returns without error for some reason.
|
||||
eventsDone chan struct{}
|
||||
|
||||
// eventsChan is the channel that the engine listens on for events from
|
||||
// the Watch loop for that resource. The event is nil normally, except
|
||||
// when events are sent on this channel from the engine. This only
|
||||
// happens as a signaling mechanism when Watch has shutdown and we want
|
||||
// to notify the Process loop which reads from this.
|
||||
eventsChan chan error // outgoing from resource
|
||||
|
||||
// pokeChan is a separate channel that the Process loop listens on to
|
||||
// know when we might need to run Process. It never closes, and is safe
|
||||
// to send on since it is buffered.
|
||||
pokeChan chan struct{} // outgoing from resource
|
||||
|
||||
// paused represents if this particular res is paused or not.
|
||||
paused bool
|
||||
// pauseSignal closes to request a pause of this resource.
|
||||
pauseSignal chan struct{}
|
||||
// resumeSignal closes to request a resume of this resource.
|
||||
resumeSignal chan struct{}
|
||||
// pausedAck is used to send an ack message saying that we've paused.
|
||||
pausedAck *util.EasyAck
|
||||
|
||||
wg *sync.WaitGroup // used for all vertex specific processes
|
||||
|
||||
cuid *converger.UID // primary converger
|
||||
tuid *converger.UID // secondary converger
|
||||
|
||||
init *engine.Init // a copy of the init struct passed to res Init
|
||||
}
|
||||
|
||||
// Init initializes structures like channels.
|
||||
func (obj *State) Init() error {
|
||||
res, isRes := obj.Vertex.(engine.Res)
|
||||
if !isRes {
|
||||
return fmt.Errorf("vertex is not a Res")
|
||||
}
|
||||
if obj.Hostname == "" {
|
||||
return fmt.Errorf("the Hostname is empty")
|
||||
}
|
||||
if obj.Prefix == "" {
|
||||
return fmt.Errorf("the Prefix is empty")
|
||||
}
|
||||
if obj.Prefix == "/" {
|
||||
return fmt.Errorf("the Prefix is root")
|
||||
}
|
||||
if obj.Logf == nil {
|
||||
return fmt.Errorf("the Logf function is missing")
|
||||
}
|
||||
|
||||
obj.doneChan = make(chan struct{})
|
||||
|
||||
obj.processDone = make(chan struct{})
|
||||
obj.watchDone = make(chan struct{})
|
||||
obj.limitDone = make(chan struct{})
|
||||
obj.removeDone = make(chan struct{})
|
||||
obj.eventsDone = make(chan struct{})
|
||||
|
||||
obj.eventsChan = make(chan error)
|
||||
|
||||
obj.pokeChan = make(chan struct{}, 1) // must be buffered
|
||||
|
||||
//obj.paused = false // starts off as started
|
||||
obj.pauseSignal = make(chan struct{})
|
||||
//obj.resumeSignal = make(chan struct{}) // happens on pause
|
||||
//obj.pausedAck = util.NewEasyAck() // happens on pause
|
||||
|
||||
obj.wg = &sync.WaitGroup{}
|
||||
|
||||
//obj.cuid = obj.Converger.Register() // gets registered in Worker()
|
||||
//obj.tuid = obj.Converger.Register() // gets registered in Worker()
|
||||
|
||||
obj.init = &engine.Init{
|
||||
Program: obj.Program,
|
||||
Hostname: obj.Hostname,
|
||||
|
||||
// Watch:
|
||||
Running: obj.event,
|
||||
Event: obj.event,
|
||||
Done: obj.doneChan,
|
||||
|
||||
// CheckApply:
|
||||
Refresh: func() bool {
|
||||
res, ok := obj.Vertex.(engine.RefreshableRes)
|
||||
if !ok {
|
||||
panic("res does not support the Refreshable trait")
|
||||
}
|
||||
return res.Refresh()
|
||||
},
|
||||
|
||||
Send: engine.GenerateSendFunc(res),
|
||||
Recv: engine.GenerateRecvFunc(res),
|
||||
|
||||
// FIXME: pass in a safe, limited query func instead?
|
||||
// TODO: not implemented, use FilteredGraph
|
||||
//Graph: func() *pgraph.Graph {
|
||||
// _, ok := obj.Vertex.(engine.CanGraphQueryRes)
|
||||
// if !ok {
|
||||
// panic("res does not support the GraphQuery trait")
|
||||
// }
|
||||
// return obj.Graph // we return in a func so it's fresh!
|
||||
//},
|
||||
|
||||
FilteredGraph: func() (*pgraph.Graph, error) {
|
||||
graph, err := pgraph.NewGraph("filtered")
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "could not create graph")
|
||||
}
|
||||
|
||||
// filter graph and build a new one...
|
||||
adjacency := obj.Graph.Adjacency()
|
||||
for v1 := range adjacency {
|
||||
// check we're allowed
|
||||
r1, ok := v1.(engine.GraphQueryableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// pass in information on requestor...
|
||||
if err := r1.GraphQueryAllowed(
|
||||
engine.GraphQueryableOptionKind(res.Kind()),
|
||||
engine.GraphQueryableOptionName(res.Name()),
|
||||
// TODO: add more information...
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
graph.AddVertex(v1)
|
||||
|
||||
for v2, edge := range adjacency[v1] {
|
||||
r2, ok := v2.(engine.GraphQueryableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// pass in information on requestor...
|
||||
if err := r2.GraphQueryAllowed(
|
||||
engine.GraphQueryableOptionKind(res.Kind()),
|
||||
engine.GraphQueryableOptionName(res.Name()),
|
||||
// TODO: add more information...
|
||||
); err != nil {
|
||||
continue
|
||||
}
|
||||
//graph.AddVertex(v2) // redundant
|
||||
graph.AddEdge(v1, v2, edge)
|
||||
}
|
||||
}
|
||||
|
||||
return graph, nil // we return in a func so it's fresh!
|
||||
},
|
||||
|
||||
World: obj.World,
|
||||
VarDir: obj.varDir,
|
||||
|
||||
Debug: obj.Debug,
|
||||
Logf: func(format string, v ...interface{}) {
|
||||
obj.Logf("resource: "+format, v...)
|
||||
},
|
||||
}
|
||||
|
||||
// run the init
|
||||
if obj.Debug {
|
||||
obj.Logf("Init(%s)", res)
|
||||
}
|
||||
|
||||
// write the reverse request to the disk...
|
||||
if err := obj.ReversalInit(); err != nil {
|
||||
return err // TODO: test this code path...
|
||||
}
|
||||
|
||||
err := res.Init(obj.init)
|
||||
if obj.Debug {
|
||||
obj.Logf("Init(%s): Return(%+v)", res, err)
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not Init() resource")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts down and performs any cleanup. This is most akin to a "post" or
|
||||
// cleanup command as the initiator for closing a vertex happens in graph sync.
|
||||
func (obj *State) Close() error {
|
||||
res, isRes := obj.Vertex.(engine.Res)
|
||||
if !isRes {
|
||||
return fmt.Errorf("vertex is not a Res")
|
||||
}
|
||||
|
||||
//if obj.cuid != nil {
|
||||
// obj.cuid.Unregister() // gets unregistered in Worker()
|
||||
//}
|
||||
//if obj.tuid != nil {
|
||||
// obj.tuid.Unregister() // gets unregistered in Worker()
|
||||
//}
|
||||
|
||||
// redundant safety
|
||||
obj.wg.Wait() // wait until all poke's and events on me have exited
|
||||
|
||||
// run the close
|
||||
if obj.Debug {
|
||||
obj.Logf("Close(%s)", res)
|
||||
}
|
||||
|
||||
var reverr error
|
||||
// clear the reverse request from the disk...
|
||||
if err := obj.ReversalClose(); err != nil {
|
||||
// TODO: test this code path...
|
||||
// TODO: should this be an error or a warning?
|
||||
reverr = err
|
||||
}
|
||||
|
||||
reterr := res.Close()
|
||||
if obj.Debug {
|
||||
obj.Logf("Close(%s): Return(%+v)", res, reterr)
|
||||
}
|
||||
|
||||
reterr = errwrap.Append(reterr, reverr)
|
||||
|
||||
return reterr
|
||||
}
|
||||
|
||||
// Poke sends a notification on the poke channel. This channel is used to notify
|
||||
// the Worker to run the Process/CheckApply when it can. This is used when there
|
||||
// is a need to schedule or reschedule some work which got postponed or dropped.
|
||||
// This doesn't contain any internal synchronization primitives or wait groups,
|
||||
// callers are expected to make sure that they don't leave any of these running
|
||||
// by the time the Worker() shuts down.
|
||||
func (obj *State) Poke() {
|
||||
// redundant
|
||||
//if len(obj.pokeChan) > 0 {
|
||||
// return
|
||||
//}
|
||||
|
||||
select {
|
||||
case obj.pokeChan <- struct{}{}:
|
||||
default: // if chan is now full because more than one poke happened...
|
||||
}
|
||||
}
|
||||
|
||||
// Pause pauses this resource. It should not be called on any already paused
|
||||
// resource. It will block until the resource pauses with an acknowledgment, or
|
||||
// until an exit for that resource is seen. If the latter happens it will error.
|
||||
// It is NOT thread-safe with the Resume() method so only call either one at a
|
||||
// time.
|
||||
func (obj *State) Pause() error {
|
||||
if obj.paused {
|
||||
return fmt.Errorf("already paused")
|
||||
}
|
||||
|
||||
obj.pausedAck = util.NewEasyAck()
|
||||
obj.resumeSignal = make(chan struct{}) // build the resume signal
|
||||
close(obj.pauseSignal)
|
||||
obj.Poke() // unblock and notice the pause if necessary
|
||||
|
||||
// wait for ack (or exit signal)
|
||||
select {
|
||||
case <-obj.pausedAck.Wait(): // we got it!
|
||||
// we're paused
|
||||
case <-obj.doneChan:
|
||||
return engine.ErrClosed
|
||||
}
|
||||
obj.paused = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resume unpauses this resource. It can be safely called on a brand-new
|
||||
// resource that has just started running without incident. It is NOT
|
||||
// thread-safe with the Pause() method, so only call either one at a time.
|
||||
func (obj *State) Resume() {
|
||||
// TODO: do we need a mutex around Resume?
|
||||
if !obj.paused { // no need to unpause brand-new resources
|
||||
return
|
||||
}
|
||||
|
||||
obj.pauseSignal = make(chan struct{}) // rebuild for next pause
|
||||
close(obj.resumeSignal)
|
||||
//obj.Poke() // not needed, we're already waiting for resume
|
||||
|
||||
obj.paused = false
|
||||
|
||||
// no need to wait for it to resume
|
||||
//return // implied
|
||||
}
|
||||
|
||||
// event is a helper function to send an event to the CheckApply process loop.
|
||||
// It can be used for the initial `running` event, or any regular event. You
|
||||
// should instead use Poke() to "schedule" a new Process/CheckApply loop when
|
||||
// one might be needed. This method will block until we're unpaused and ready to
|
||||
// receive on the events channel.
|
||||
func (obj *State) event() {
|
||||
obj.setDirty() // assume we're initially dirty
|
||||
|
||||
select {
|
||||
case obj.eventsChan <- nil:
|
||||
// send!
|
||||
}
|
||||
|
||||
//return // implied
|
||||
}
|
||||
|
||||
// setDirty marks the resource state as dirty. This signals to the engine that
|
||||
// CheckApply will have some work to do in order to converge it.
|
||||
func (obj *State) setDirty() {
|
||||
obj.tuid.StopTimer()
|
||||
obj.isStateOK = false
|
||||
}
|
||||
|
||||
// poll is a replacement for Watch when the Poll metaparameter is used.
|
||||
func (obj *State) poll(interval uint32) error {
|
||||
// create a time.Ticker for the given interval
|
||||
ticker := time.NewTicker(time.Duration(interval) * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C: // received the timer event
|
||||
obj.init.Logf("polling...")
|
||||
|
||||
case <-obj.init.Done: // signal for shutdown request
|
||||
return nil
|
||||
}
|
||||
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
51
engine/graph/vardir.go
Normal file
51
engine/graph/vardir.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// varDir returns the path to a working directory for the resource. It will try
|
||||
// and create the directory first, and return an error if this failed. The dir
|
||||
// should be cleaned up by the resource on Close if it wishes to discard the
|
||||
// contents. If it does not, then a future resource with the same kind and name
|
||||
// may see those contents in that directory. The resource should clean up the
|
||||
// contents before use if it is important that nothing exist. It is always
|
||||
// possible that contents could remain after an abrupt crash, so do not store
|
||||
// overly sensitive data unless you're aware of the risks.
|
||||
func (obj *State) varDir(extra string) (string, error) {
|
||||
// Using extra adds additional dirs onto our namespace. An empty extra
|
||||
// adds no additional directories.
|
||||
if obj.Prefix == "" { // safety
|
||||
return "", fmt.Errorf("the VarDir prefix is empty")
|
||||
}
|
||||
|
||||
// an empty string at the end has no effect
|
||||
p := fmt.Sprintf("%s/", path.Join(obj.Prefix, extra))
|
||||
if err := os.MkdirAll(p, 0770); err != nil {
|
||||
return "", errwrap.Wrapf(err, "can't create prefix in: %s", p)
|
||||
}
|
||||
|
||||
// returns with a trailing slash as per the mgmt file res convention
|
||||
return p, nil
|
||||
}
|
||||
70
engine/graphqueryable.go
Normal file
70
engine/graphqueryable.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
// GraphQueryableRes is the interface that must be implemented if you want your
|
||||
// resource to be allowed to be queried from another resource in the graph. This
|
||||
// is done as a form of explicit authorization tracking so that we can consider
|
||||
// security aspects more easily. Ultimately, all resource code should be
|
||||
// trusted, but it's still a good idea to know if a particular resource is even
|
||||
// able to access information about another one, and if your resource doesn't
|
||||
// add the trait supporting this, then it won't be allowed.
|
||||
type GraphQueryableRes interface {
|
||||
Res // implement everything in Res but add the additional requirements
|
||||
|
||||
// GraphQueryAllowed returns nil if you're allowed to query the graph.
|
||||
GraphQueryAllowed(...GraphQueryableOption) error
|
||||
}
|
||||
|
||||
// GraphQueryableOption is an option that can be used to specify the
|
||||
// authentication.
|
||||
type GraphQueryableOption func(*GraphQueryableOptions)
|
||||
|
||||
// GraphQueryableOptions represents the different possible configurable options.
|
||||
type GraphQueryableOptions struct {
|
||||
// Kind is the kind of the resource making the access.
|
||||
Kind string
|
||||
// Name is the name of the resource making the access.
|
||||
Name string
|
||||
// TODO: add more options if needed
|
||||
}
|
||||
|
||||
// Apply is a helper function to apply a list of options to the struct. You
|
||||
// should initialize it with defaults you want, and then apply any you've
|
||||
// received like this.
|
||||
func (obj *GraphQueryableOptions) Apply(opts ...GraphQueryableOption) {
|
||||
for _, optionFunc := range opts { // apply the options
|
||||
optionFunc(obj)
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQueryableOptionKind tells the GraphQueryAllowed function what the
|
||||
// resource kind is.
|
||||
func GraphQueryableOptionKind(kind string) GraphQueryableOption {
|
||||
return func(gqo *GraphQueryableOptions) {
|
||||
gqo.Kind = kind
|
||||
}
|
||||
}
|
||||
|
||||
// GraphQueryableOptionName tells the GraphQueryAllowed function what the
|
||||
// resource name is.
|
||||
func GraphQueryableOptionName(name string) GraphQueryableOption {
|
||||
return func(gqo *GraphQueryableOptions) {
|
||||
gqo.Name = name
|
||||
}
|
||||
}
|
||||
202
engine/metaparams.go
Normal file
202
engine/metaparams.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// DefaultMetaParams are the defaults that are used for undefined metaparams.
|
||||
// Don't modify this variable. Use .Copy() if you'd like some for yourself.
|
||||
var DefaultMetaParams = &MetaParams{
|
||||
Noop: false,
|
||||
Retry: 0,
|
||||
Delay: 0,
|
||||
Poll: 0, // defaults to watching for events
|
||||
Limit: rate.Inf, // defaults to no limit
|
||||
Burst: 0, // no burst needed on an infinite rate
|
||||
//Sema: []string{},
|
||||
Rewatch: true,
|
||||
Realize: false, // true would be more awesome, but unexpected for users
|
||||
}
|
||||
|
||||
// MetaRes is the interface a resource must implement to support meta params.
|
||||
// All resources must implement this.
|
||||
type MetaRes interface {
|
||||
// MetaParams lets you get or set meta params for the resource.
|
||||
MetaParams() *MetaParams
|
||||
|
||||
// SetMetaParams lets you set all of the meta params for the resource in
|
||||
// a single call.
|
||||
SetMetaParams(*MetaParams)
|
||||
}
|
||||
|
||||
// MetaParams provides some meta parameters that apply to every resource.
|
||||
type MetaParams struct {
|
||||
// Noop specifies that no changes should be made by the resource. It
|
||||
// relies on the individual resource implementation, and can't protect
|
||||
// you from a poorly or maliciously implemented resource.
|
||||
Noop bool `yaml:"noop"`
|
||||
|
||||
// NOTE: there are separate Watch and CheckApply retry and delay values,
|
||||
// but I've decided to use the same ones for both until there's a proper
|
||||
// reason to want to do something differently for the Watch errors.
|
||||
|
||||
// Retry is the number of times to retry on error. Use -1 for infinite.
|
||||
Retry int16 `yaml:"retry"`
|
||||
|
||||
// Delay is the number of milliseconds to wait between retries.
|
||||
Delay uint64 `yaml:"delay"`
|
||||
|
||||
// Poll is the number of seconds between poll intervals. Use 0 to Watch.
|
||||
Poll uint32 `yaml:"poll"`
|
||||
|
||||
// Limit is the number of events per second to allow through.
|
||||
Limit rate.Limit `yaml:"limit"`
|
||||
|
||||
// Burst is the number of events to allow in a burst.
|
||||
Burst int `yaml:"burst"`
|
||||
|
||||
// Sema is a list of semaphore ids in the form `id` or `id:count`. If
|
||||
// you don't specify a count, then 1 is assumed. The sema of `foo` which
|
||||
// has a count equal to 1, is different from a sema named `foo:1` which
|
||||
// also has a count equal to 1, but is a different semaphore.
|
||||
Sema []string `yaml:"sema"`
|
||||
|
||||
// Rewatch specifies whether we re-run the Watch worker during a swap
|
||||
// if it has errored. When doing a GraphCmp to swap the graphs, if this
|
||||
// is true, and this particular worker has errored, then we'll remove it
|
||||
// and add it back as a new vertex, thus causing it to run again. This
|
||||
// is different from the Retry metaparam which applies during the normal
|
||||
// execution. It is only when this is exhausted that we're in permanent
|
||||
// worker failure, and only then can we rely on this metaparam.
|
||||
Rewatch bool `yaml:"rewatch"`
|
||||
|
||||
// Realize ensures that the resource is guaranteed to converge at least
|
||||
// once before a potential graph swap removes or changes it. This
|
||||
// guarantee is useful for fast changing graphs, to ensure that the
|
||||
// brief creation of a resource is seen. This guarantee does not prevent
|
||||
// against the engine quitting normally, and it can't guarantee it if
|
||||
// the resource is blocked because of a failed pre-requisite resource.
|
||||
// XXX: Not implemented!
|
||||
Realize bool `yaml:"realize"`
|
||||
}
|
||||
|
||||
// Cmp compares two AutoGroupMeta structs and determines if they're equivalent.
|
||||
func (obj *MetaParams) Cmp(meta *MetaParams) error {
|
||||
if obj.Noop != meta.Noop {
|
||||
return fmt.Errorf("values for Noop are different")
|
||||
}
|
||||
// XXX: add a one way cmp like we used to have ?
|
||||
//if obj.Noop != meta.Noop {
|
||||
// // obj is the existing res, res is the *new* resource
|
||||
// // if we go from no-noop -> noop, we can re-use the obj
|
||||
// // if we go from noop -> no-noop, we need to regenerate
|
||||
// if obj.Noop { // asymmetrical
|
||||
// return fmt.Errorf("values for Noop are different") // going from noop to no-noop!
|
||||
// }
|
||||
//}
|
||||
|
||||
if obj.Retry != meta.Retry {
|
||||
return fmt.Errorf("values for Retry are different")
|
||||
}
|
||||
if obj.Delay != meta.Delay {
|
||||
return fmt.Errorf("values for Delay are different")
|
||||
}
|
||||
if obj.Poll != meta.Poll {
|
||||
return fmt.Errorf("values for Poll are different")
|
||||
}
|
||||
if obj.Limit != meta.Limit {
|
||||
return fmt.Errorf("values for Limit are different")
|
||||
}
|
||||
if obj.Burst != meta.Burst {
|
||||
return fmt.Errorf("values for Burst are different")
|
||||
}
|
||||
|
||||
if err := util.SortedStrSliceCompare(obj.Sema, meta.Sema); err != nil {
|
||||
return errwrap.Wrapf(err, "values for Sema are different")
|
||||
}
|
||||
|
||||
if obj.Rewatch != meta.Rewatch {
|
||||
return fmt.Errorf("values for Rewatch are different")
|
||||
}
|
||||
if obj.Realize != meta.Realize {
|
||||
return fmt.Errorf("values for Realize are different")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate runs some validation on the meta params.
|
||||
func (obj *MetaParams) Validate() error {
|
||||
if obj.Burst == 0 && !(obj.Limit == rate.Inf) { // blocked
|
||||
return fmt.Errorf("permanently limited (rate != Inf, burst = 0)")
|
||||
}
|
||||
|
||||
for _, s := range obj.Sema {
|
||||
if s == "" {
|
||||
return fmt.Errorf("semaphore is empty")
|
||||
}
|
||||
if _, err := strconv.Atoi(s); err == nil { // standalone int
|
||||
return fmt.Errorf("semaphore format is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy copies this struct and returns a new one.
|
||||
func (obj *MetaParams) Copy() *MetaParams {
|
||||
sema := []string{}
|
||||
if obj.Sema != nil {
|
||||
sema = make([]string, len(obj.Sema))
|
||||
copy(sema, obj.Sema)
|
||||
}
|
||||
return &MetaParams{
|
||||
Noop: obj.Noop,
|
||||
Retry: obj.Retry,
|
||||
Delay: obj.Delay,
|
||||
Poll: obj.Poll,
|
||||
Limit: obj.Limit, // FIXME: can we copy this type like this? test me!
|
||||
Burst: obj.Burst,
|
||||
Sema: sema,
|
||||
Rewatch: obj.Rewatch,
|
||||
Realize: obj.Realize,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for the MetaParams struct. It
|
||||
// is primarily useful for setting the defaults.
|
||||
// TODO: this is untested
|
||||
func (obj *MetaParams) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawMetaParams MetaParams // indirection to avoid infinite recursion
|
||||
raw := rawMetaParams(*DefaultMetaParams) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = MetaParams(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
42
engine/metaparams_test.go
Normal file
42
engine/metaparams_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMetaCmp1(t *testing.T) {
|
||||
m1 := &MetaParams{
|
||||
Noop: true,
|
||||
}
|
||||
m2 := &MetaParams{
|
||||
Noop: false,
|
||||
}
|
||||
|
||||
// TODO: should we allow this? Maybe only with the future Mutate API?
|
||||
//if err := m2.Cmp(m1); err != nil { // going from noop(false) -> noop(true) is okay!
|
||||
// t.Errorf("the two resources do not match")
|
||||
//}
|
||||
|
||||
if m1.Cmp(m2) == nil { // going from noop(true) -> noop(false) is not okay!
|
||||
t.Errorf("the two resources should not match")
|
||||
}
|
||||
}
|
||||
32
engine/refresh.go
Normal file
32
engine/refresh.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
// RefreshableRes is the interface a resource must implement to support refresh
|
||||
// notifications. Default implementations for all of the methods declared in
|
||||
// this interface can be obtained for your resource by anonymously adding the
|
||||
// traits.Refreshable struct to your resource implementation.
|
||||
type RefreshableRes interface {
|
||||
Res // implement everything in Res but add the additional requirements
|
||||
|
||||
// Refresh returns the refresh notification state.
|
||||
Refresh() bool
|
||||
|
||||
// SetRefresh sets the refresh notification state.
|
||||
SetRefresh(bool)
|
||||
}
|
||||
314
engine/resources.go
Normal file
314
engine/resources.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// TODO: should each resource be a sub-package?
|
||||
var registeredResources = map[string]func() Res{}
|
||||
|
||||
// RegisterResource registers a new resource by providing a constructor function
|
||||
// that returns a resource object ready to be unmarshalled from YAML.
|
||||
func RegisterResource(kind string, fn func() Res) {
|
||||
f := fn()
|
||||
if kind == "" {
|
||||
panic("can't register a resource with an empty kind")
|
||||
}
|
||||
if _, ok := registeredResources[kind]; ok {
|
||||
panic(fmt.Sprintf("a resource kind of %s is already registered", kind))
|
||||
}
|
||||
gob.Register(f)
|
||||
registeredResources[kind] = fn
|
||||
}
|
||||
|
||||
// RegisteredResourcesNames returns the kind of the registered resources.
|
||||
func RegisteredResourcesNames() []string {
|
||||
kinds := []string{}
|
||||
for k := range registeredResources {
|
||||
kinds = append(kinds, k)
|
||||
}
|
||||
return kinds
|
||||
}
|
||||
|
||||
// NewResource returns an empty resource object from a registered kind. It
|
||||
// errors if the resource kind doesn't exist.
|
||||
func NewResource(kind string) (Res, error) {
|
||||
fn, ok := registeredResources[kind]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no resource kind `%s` available", kind)
|
||||
}
|
||||
res := fn().Default()
|
||||
res.SetKind(kind)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// NewNamedResource returns an empty resource object from a registered kind. It
|
||||
// also sets the name. It is a wrapper around NewResource. It also errors if the
|
||||
// name is empty.
|
||||
func NewNamedResource(kind, name string) (Res, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("resource name is empty")
|
||||
}
|
||||
res, err := NewResource(kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.SetName(name)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Init is the structure of values and references which is passed into all
|
||||
// resources on initialization. None of these are available in Validate, or
|
||||
// before Init runs.
|
||||
type Init struct {
|
||||
// Program is the name of the program.
|
||||
Program string
|
||||
|
||||
// Hostname is the uuid for the host.
|
||||
Hostname string
|
||||
|
||||
// Called from within Watch:
|
||||
|
||||
// Running must be called after your watches are all started and ready.
|
||||
Running func()
|
||||
|
||||
// Event sends an event notifying the engine of a possible state change.
|
||||
Event func()
|
||||
|
||||
// Done returns a channel that will close to signal to us that it's time
|
||||
// for us to shutdown.
|
||||
Done chan struct{}
|
||||
|
||||
// Called from within CheckApply:
|
||||
|
||||
// Refresh returns whether the resource received a notification. This
|
||||
// flag can be used to tell a svc to reload, or to perform some state
|
||||
// change that wouldn't otherwise be noticed by inspection alone. You
|
||||
// must implement the Refreshable trait for this to work.
|
||||
Refresh func() bool
|
||||
|
||||
// Send exposes some variables you wish to send via the Send/Recv
|
||||
// mechanism. You must implement the Sendable trait for this to work.
|
||||
Send func(interface{}) error
|
||||
|
||||
// Recv provides a map of variables which were sent to this resource via
|
||||
// the Send/Recv mechanism. You must implement the Recvable trait for
|
||||
// this to work.
|
||||
Recv func() map[string]*Send
|
||||
|
||||
// Other functionality:
|
||||
|
||||
// Graph is a function that returns the current graph. The returned
|
||||
// value won't be valid after a graphsync so make sure to call this when
|
||||
// you are about to use it, and discard it right after.
|
||||
// FIXME: it might be better to offer a safer, more limited, GraphQuery?
|
||||
//Graph func() *pgraph.Graph // TODO: not implemented, use FilteredGraph
|
||||
|
||||
// FilteredGraph is a function that returns a filtered variant of the
|
||||
// current graph. Only resource that have allowed themselves to be added
|
||||
// into this graph will appear. If they did not consent, then those
|
||||
// vertices and any associated edges, will not be present.
|
||||
FilteredGraph func() (*pgraph.Graph, error)
|
||||
|
||||
// TODO: GraphQuery offers an interface to query the resource graph.
|
||||
|
||||
// World provides a connection to the outside world. This is most often
|
||||
// used for communicating with the distributed database.
|
||||
World World
|
||||
|
||||
// VarDir is a facility for local storage. It is used to return a path
|
||||
// to a directory which may be used for temporary storage. It should be
|
||||
// cleaned up on resource Close if the resource would like to delete the
|
||||
// contents. The resource should not assume that the initial directory
|
||||
// is empty, and it should be cleaned on Init if that is a requirement.
|
||||
VarDir func(string) (string, error)
|
||||
|
||||
// Debug signals whether we are running in debugging mode. In this case,
|
||||
// we might want to log additional messages.
|
||||
Debug bool
|
||||
|
||||
// Logf is a logging facility which will correctly namespace any
|
||||
// messages which you wish to pass on. You should use this instead of
|
||||
// the log package directly for production quality resources.
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// KindedRes is an interface that is required for a resource to have a kind.
|
||||
type KindedRes interface {
|
||||
// Kind returns a string representing the kind of resource this is.
|
||||
Kind() string
|
||||
|
||||
// SetKind sets the resource kind and should only be called by the
|
||||
// engine.
|
||||
SetKind(string)
|
||||
}
|
||||
|
||||
// NamedRes is an interface that is used so a resource can have a unique name.
|
||||
type NamedRes interface {
|
||||
Name() string
|
||||
SetName(string)
|
||||
}
|
||||
|
||||
// Res is the minimum interface you need to implement to define a new resource.
|
||||
type Res interface {
|
||||
fmt.Stringer // String() string
|
||||
|
||||
KindedRes
|
||||
NamedRes // TODO: consider making this optional in the future
|
||||
MetaRes // All resources must have meta params.
|
||||
|
||||
// Default returns a struct with sane defaults for this resource.
|
||||
Default() Res
|
||||
|
||||
// Validate determines if the struct has been defined in a valid state.
|
||||
Validate() error
|
||||
|
||||
// Init initializes the resource and passes in some external information
|
||||
// and data from the engine.
|
||||
Init(*Init) error
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
Close() error
|
||||
|
||||
// Watch is run by the engine to monitor for state changes. If it
|
||||
// detects any, it notifies the engine which will usually run CheckApply
|
||||
// in response.
|
||||
Watch() error
|
||||
|
||||
// CheckApply determines if the state of the resource is correct and if
|
||||
// asked to with the `apply` variable, applies the requested state.
|
||||
CheckApply(apply bool) (checkOK bool, err error)
|
||||
|
||||
// Cmp compares itself to another resource and returns an error if they
|
||||
// are not equivalent. This is more strict than the Adapts method of the
|
||||
// CompatibleRes interface which allows for equivalent differences if
|
||||
// the have a compatible result in CheckApply.
|
||||
Cmp(Res) error
|
||||
}
|
||||
|
||||
// Repr returns a representation of a resource from its kind and name. This is
|
||||
// used as the definitive format so that it can be changed in one place.
|
||||
func Repr(kind, name string) string {
|
||||
return fmt.Sprintf("%s[%s]", kind, name)
|
||||
}
|
||||
|
||||
// Stringer returns a consistent and unique string representation of a resource.
|
||||
func Stringer(res Res) string {
|
||||
return Repr(res.Kind(), res.Name())
|
||||
}
|
||||
|
||||
// Validate validates a resource by checking multiple aspects. This is the main
|
||||
// entry point for running all the validation steps on a resource.
|
||||
func Validate(res Res) error {
|
||||
if res.Kind() == "" { // shouldn't happen IIRC
|
||||
return fmt.Errorf("the Res has an empty Kind")
|
||||
}
|
||||
if res.Name() == "" {
|
||||
return fmt.Errorf("the Res has an empty Name")
|
||||
}
|
||||
|
||||
if err := res.MetaParams().Validate(); err != nil {
|
||||
return errwrap.Wrapf(err, "the Res has an invalid meta param")
|
||||
}
|
||||
|
||||
return res.Validate()
|
||||
}
|
||||
|
||||
// InterruptableRes is an interface that adds interrupt functionality to
|
||||
// resources. If the resource implements this interface, the engine will call
|
||||
// the Interrupt method to shutdown the resource quickly. Running this method
|
||||
// may leave the resource in a partial state, however this may be desired if you
|
||||
// want a faster exit or if you'd prefer a partial state over letting the
|
||||
// resource complete in a situation where you made an error and you wish to exit
|
||||
// quickly to avoid data loss. It is usually triggered after multiple ^C
|
||||
// signals.
|
||||
type InterruptableRes interface {
|
||||
Res
|
||||
|
||||
// Ask the resource to shutdown quickly. This can be called at any point
|
||||
// in the resource lifecycle after Init. Close will still be called. It
|
||||
// will only get called after an exit or pause request has been made. It
|
||||
// is designed to unblock any long running operation that is occurring
|
||||
// in the CheckApply portion of the life cycle. If the resource has
|
||||
// already exited, running this method should not block. (That is to say
|
||||
// that you should not expect CheckApply or Watch to be alive and be
|
||||
// able to read from a channel to satisfy your request.) It is best to
|
||||
// probably have this close a channel to multicast that signal around to
|
||||
// anyone who can detect it in a select. If you are in a situation which
|
||||
// cannot interrupt, then you can return an error.
|
||||
// FIXME: implement, and check the above description is what we expect!
|
||||
Interrupt() error
|
||||
}
|
||||
|
||||
// CopyableRes is an interface that a resource can implement if we want to be
|
||||
// able to copy the resource to build another one.
|
||||
type CopyableRes interface {
|
||||
Res
|
||||
|
||||
// Copy returns a new resource which has a copy of the public data.
|
||||
// Don't call this directly, use engine.ResCopy instead.
|
||||
// TODO: should we copy any private state or not?
|
||||
Copy() CopyableRes
|
||||
}
|
||||
|
||||
// CompatibleRes is an interface that a resource can implement to express if a
|
||||
// similar variant of itself is functionally equivalent. For example, two `pkg`
|
||||
// resources that install `cowsay` could be equivalent if one requests a state
|
||||
// of `installed` and the other requests `newest`, since they'll finish with a
|
||||
// compatible result. This doesn't need to be behind a metaparam flag or trait,
|
||||
// because it is never beneficial to turn it off, unless there is a bug to fix.
|
||||
type CompatibleRes interface {
|
||||
//Res // causes "duplicate method" error
|
||||
CopyableRes // we'll need to use the Copy method in the Merge function!
|
||||
|
||||
// Adapts compares itself to another resource and returns an error if
|
||||
// they are not compatibly equivalent. This is less strict than the
|
||||
// default `Cmp` method which should be used for most cases. Don't call
|
||||
// this directly, use engine.AdaptCmp instead.
|
||||
Adapts(CompatibleRes) error
|
||||
|
||||
// Merge returns the combined resource to use when two are equivalent.
|
||||
// This might get called multiple times for N different resources that
|
||||
// need to get merged, and so it should produce a consistent result no
|
||||
// matter which order it is called in. Don't call this directly, use
|
||||
// engine.ResMerge instead.
|
||||
Merge(CompatibleRes) (CompatibleRes, error)
|
||||
}
|
||||
|
||||
// CollectableRes is an interface for resources that support collection. It is
|
||||
// currently temporary until a proper API for all resources is invented.
|
||||
type CollectableRes interface {
|
||||
Res
|
||||
|
||||
CollectPattern(string) // XXX: temporary until Res collection is more advanced
|
||||
}
|
||||
|
||||
// YAMLRes is a resource that supports creation by unmarshalling.
|
||||
type YAMLRes interface {
|
||||
Res
|
||||
|
||||
yaml.Unmarshaler // UnmarshalYAML(unmarshal func(interface{}) error) error
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -21,13 +21,14 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
// FIXME: we vendor go/augeas because master requires augeas 1.6.0
|
||||
// and libaugeas-dev-1.6.0 is not yet available in a PPA.
|
||||
"honnef.co/go/augeas"
|
||||
@@ -39,13 +40,15 @@ const (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("augeas", func() Res { return &AugeasRes{} })
|
||||
engine.RegisterResource("augeas", func() engine.Res { return &AugeasRes{} })
|
||||
}
|
||||
|
||||
// AugeasRes is a resource that enables you to use the augeas resource.
|
||||
// Currently only allows you to change simple files (e.g sshd_config).
|
||||
type AugeasRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// File is the path to the file targeted by this resource.
|
||||
File string `yaml:"file"`
|
||||
@@ -57,7 +60,7 @@ type AugeasRes struct {
|
||||
// Sets is a list of changes that will be applied to the file, in the form of
|
||||
// ["path", "value"]. mgmt will run augeas.Get() before augeas.Set(), to
|
||||
// prevent changing the file when it is not needed.
|
||||
Sets []AugeasSet `yaml:"sets"`
|
||||
Sets []*AugeasSet `yaml:"sets"`
|
||||
|
||||
recWatcher *recwatch.RecWatcher // used to watch the changed files
|
||||
}
|
||||
@@ -68,13 +71,31 @@ type AugeasSet struct {
|
||||
Value string `yaml:"value"` // The value to be set on the given Path.
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *AugeasRes) Default() Res {
|
||||
return &AugeasRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
// Cmp compares this set with another one.
|
||||
func (obj *AugeasSet) Cmp(set *AugeasSet) error {
|
||||
if obj == nil && set == nil {
|
||||
return nil
|
||||
}
|
||||
if obj == nil && set != nil {
|
||||
return fmt.Errorf("can't compare nil set to set")
|
||||
}
|
||||
if obj != nil && set == nil {
|
||||
return fmt.Errorf("can't compare set to nil set")
|
||||
}
|
||||
|
||||
if obj.Path != set.Path {
|
||||
return fmt.Errorf("the Path values differ")
|
||||
}
|
||||
if obj.Value != set.Value {
|
||||
return fmt.Errorf("the Value values differ")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *AugeasRes) Default() engine.Res {
|
||||
return &AugeasRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
@@ -88,16 +109,23 @@ func (obj *AugeasRes) Validate() error {
|
||||
if (obj.Lens == "") != (obj.File == "") {
|
||||
return fmt.Errorf("the File and Lens params must be specified together")
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initiates the resource.
|
||||
func (obj *AugeasRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
// Init initializes the resource.
|
||||
func (obj *AugeasRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
// Taken from the File resource.
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *AugeasRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events. This
|
||||
// was taken from the File resource.
|
||||
// FIXME: DRY - This is taken from the file resource
|
||||
func (obj *AugeasRes) Watch() error {
|
||||
var err error
|
||||
@@ -107,17 +135,12 @@ func (obj *AugeasRes) Watch() error {
|
||||
}
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching: %s", obj, obj.File) // attempting to watch...
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", obj.File) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -128,29 +151,25 @@ func (obj *AugeasRes) Watch() error {
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.debug { // don't access event.Body if event.Error isn't nil
|
||||
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op)
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
//obj.StateOK(false) // dirty // these events don't invalidate state
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkApplySet runs CheckApply for one element of the AugeasRes.Set
|
||||
func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set AugeasSet) (bool, error) {
|
||||
func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set *AugeasSet) (bool, error) {
|
||||
fullpath := fmt.Sprintf("/files/%v/%v", obj.File, set.Path)
|
||||
|
||||
// We do not check for errors because errors are also thrown when
|
||||
@@ -176,7 +195,7 @@ func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set AugeasSet
|
||||
|
||||
// CheckApply method for Augeas resource.
|
||||
func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
|
||||
log.Printf("%s: CheckApply: %s", obj, obj.File)
|
||||
obj.init.Logf("CheckApply: %s", obj.File)
|
||||
// By default we do not set any option to augeas, we use the defaults.
|
||||
opts := augeas.None
|
||||
if obj.Lens != "" {
|
||||
@@ -224,7 +243,7 @@ func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
log.Printf("%s: changes needed, saving", obj)
|
||||
obj.init.Logf("changes needed, saving")
|
||||
if err = ag.Save(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while saving augeas values")
|
||||
}
|
||||
@@ -240,45 +259,50 @@ func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *AugeasRes) Cmp(r engine.Res) error {
|
||||
// we can only compare to others of the same resource kind
|
||||
res, ok := r.(*AugeasRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("resource is not the same kind")
|
||||
}
|
||||
|
||||
if obj.File != res.File {
|
||||
return fmt.Errorf("the File params differ")
|
||||
}
|
||||
if obj.Lens != res.Lens {
|
||||
return fmt.Errorf("the Lens params differ")
|
||||
}
|
||||
|
||||
if len(obj.Sets) != len(res.Sets) {
|
||||
return fmt.Errorf("the length of the two Sets params differs")
|
||||
}
|
||||
for i := 0; i < len(obj.Sets); i++ {
|
||||
if err := obj.Sets[i].Cmp(res.Sets[i]); err != nil {
|
||||
return errwrap.Wrapf(err, "the Sets item at index %d differs", i)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AugeasUID is the UID struct for AugeasRes.
|
||||
type AugeasUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
func (obj *AugeasRes) UIDs() []ResUID {
|
||||
func (obj *AugeasRes) UIDs() []engine.ResUID {
|
||||
x := &AugeasUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []ResUID{x}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *AugeasRes) GroupCmp(r Res) bool {
|
||||
return false // Augeas commands can not be grouped together.
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *AugeasRes) Compare(r Res) bool {
|
||||
// we can only compare AugeasRes to others of the same resource kind
|
||||
res, ok := r.(*AugeasRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *AugeasRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes AugeasRes // indirection to avoid infinite recursion
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
250
engine/resources/config_etcd.go
Normal file
250
engine/resources/config_etcd.go
Normal file
@@ -0,0 +1,250 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("config:etcd", func() engine.Res { return &ConfigEtcdRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
sizeCheckApplyTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// ConfigEtcdRes is a resource that sets mgmt's etcd configuration.
|
||||
type ConfigEtcdRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// IdealClusterSize is the requested minimum size of the cluster. If you
|
||||
// set this to zero, it will cause a cluster wide shutdown if
|
||||
// AllowSizeShutdown is true. If it's not true, then it will cause a
|
||||
// validation error.
|
||||
IdealClusterSize uint16 `lang:"idealclustersize"`
|
||||
// AllowSizeShutdown is a required safety flag that you must set to true
|
||||
// if you want to allow causing a cluster shutdown by setting
|
||||
// IdealClusterSize to zero.
|
||||
AllowSizeShutdown bool `lang:"allow_size_shutdown"`
|
||||
|
||||
// sizeFlag determines whether sizeCheckApply already ran or not.
|
||||
sizeFlag bool
|
||||
|
||||
interruptChan chan struct{}
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *ConfigEtcdRes) Default() engine.Res {
|
||||
return &ConfigEtcdRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *ConfigEtcdRes) Validate() error {
|
||||
if obj.IdealClusterSize < 0 {
|
||||
return fmt.Errorf("the IdealClusterSize param must be positive")
|
||||
}
|
||||
|
||||
if obj.IdealClusterSize == 0 && !obj.AllowSizeShutdown {
|
||||
return fmt.Errorf("the IdealClusterSize can't be zero if AllowSizeShutdown is false")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *ConfigEtcdRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
obj.interruptChan = make(chan struct{})
|
||||
obj.wg = &sync.WaitGroup{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *ConfigEtcdRes) Close() error {
|
||||
obj.wg.Wait() // bonus
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *ConfigEtcdRes) Watch() error {
|
||||
obj.wg.Add(1)
|
||||
defer obj.wg.Done()
|
||||
// FIXME: add timeout to context
|
||||
// The obj.init.Done channel is closed by the engine to signal shutdown.
|
||||
ctx, cancel := util.ContextWithCloser(context.Background(), obj.init.Done)
|
||||
defer cancel()
|
||||
ch, err := obj.init.World.IdealClusterSizeWatch(util.CtxWithWg(ctx, obj.wg))
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not watch ideal cluster size")
|
||||
}
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-ch:
|
||||
if !ok {
|
||||
break Loop
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("event: %+v", event)
|
||||
}
|
||||
// pass through and send an event
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sizeCheckApply sets the IdealClusterSize parameter. If it sees a value change
|
||||
// to zero, then it *won't* try and change it away from zero, because it assumes
|
||||
// that someone has requested a shutdown. If the value is seen on first startup,
|
||||
// then it will change it, because it might be a zero from the previous cluster.
|
||||
func (obj *ConfigEtcdRes) sizeCheckApply(apply bool) (bool, error) {
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait() // this must be above the defer cancel() call
|
||||
ctx, cancel := context.WithTimeout(context.Background(), sizeCheckApplyTimeout)
|
||||
defer cancel()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-obj.interruptChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// let this exit
|
||||
}
|
||||
}()
|
||||
|
||||
val, err := obj.init.World.IdealClusterSizeGet(ctx)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "could not get ideal cluster size")
|
||||
}
|
||||
|
||||
// if we got a value of zero, and we've already run before, then it's ok
|
||||
if obj.IdealClusterSize != 0 && val == 0 && obj.sizeFlag {
|
||||
obj.init.Logf("impending cluster shutdown, not setting ideal cluster size")
|
||||
return true, nil // impending shutdown, don't try and cancel it.
|
||||
}
|
||||
obj.sizeFlag = true
|
||||
|
||||
// must be done after setting the above flag
|
||||
if obj.IdealClusterSize == val { // state is correct
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// set!
|
||||
// This is run as a transaction so we detect if we needed to change it.
|
||||
changed, err := obj.init.World.IdealClusterSizeSet(ctx, obj.IdealClusterSize)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "could not set ideal cluster size")
|
||||
}
|
||||
if !changed {
|
||||
return true, nil // we lost a race, which means no change needed
|
||||
}
|
||||
obj.init.Logf("set dynamic cluster size to: %d", obj.IdealClusterSize)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CheckApply method for Noop resource. Does nothing, returns happy!
|
||||
func (obj *ConfigEtcdRes) CheckApply(apply bool) (bool, error) {
|
||||
checkOK := true
|
||||
|
||||
if c, err := obj.sizeCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
// TODO: add more config settings management here...
|
||||
//if c, err := obj.TODOCheckApply(apply); err != nil {
|
||||
// return false, err
|
||||
//} else if !c {
|
||||
// checkOK = false
|
||||
//}
|
||||
|
||||
return checkOK, nil // w00t
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *ConfigEtcdRes) Cmp(r engine.Res) error {
|
||||
// we can only compare ConfigEtcdRes to others of the same resource kind
|
||||
res, ok := r.(*ConfigEtcdRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.IdealClusterSize != res.IdealClusterSize {
|
||||
return fmt.Errorf("the IdealClusterSize param differs")
|
||||
}
|
||||
if obj.AllowSizeShutdown != res.AllowSizeShutdown {
|
||||
return fmt.Errorf("the AllowSizeShutdown param differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interrupt is called to ask the execution of this resource to end early.
|
||||
func (obj *ConfigEtcdRes) Interrupt() error {
|
||||
close(obj.interruptChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *ConfigEtcdRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes ConfigEtcdRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*ConfigEtcdRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to ConfigEtcdRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = ConfigEtcdRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
283
engine/resources/consul_kv.go
Normal file
283
engine/resources/consul_kv.go
Normal file
@@ -0,0 +1,283 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("consul:kv", func() engine.Res { return &ConsulKVRes{} })
|
||||
}
|
||||
|
||||
// ConsulKVRes is a resource that writes a value into a Consul datastore. The
|
||||
// name of the resource can either be the key name, or the concatenation of the
|
||||
// server address and the key name: http://127.0.0.1:8500/my-key. If the param
|
||||
// keys are specified, then those are used. If the Name cannot be properly
|
||||
// parsed by url.Parse, then it will be considered as the Key's value. If the
|
||||
// Key is specified explicitly, then we won't use anything from the Name.
|
||||
type ConsulKVRes struct {
|
||||
traits.Base
|
||||
init *engine.Init
|
||||
|
||||
// Key is the name of the key. Defaults to the name of the resource.
|
||||
Key string `lang:"key" yaml:"key"`
|
||||
|
||||
// Value is the value for the key.
|
||||
Value string `lang:"value" yaml:"value"`
|
||||
|
||||
// Scheme is the URI scheme for the Consul server. Default: http.
|
||||
Scheme string `lang:"scheme" yaml:"scheme"`
|
||||
|
||||
// Address is the address of the Consul server. Default: 127.0.0.1:8500.
|
||||
Address string `lang:"address" yaml:"address"`
|
||||
|
||||
// Token is used to provide an ACL token to use for this resource.
|
||||
Token string `lang:"token" yaml:"token"`
|
||||
|
||||
client *api.Client
|
||||
config *api.Config // needed to close the idle connections
|
||||
once bool // safety token
|
||||
key string // cache the key name to avoid re-running the parser
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *ConsulKVRes) Default() engine.Res {
|
||||
return &ConsulKVRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *ConsulKVRes) Validate() error {
|
||||
s, _, k := obj.inputParser()
|
||||
if k == "" {
|
||||
return fmt.Errorf("the Key is empty")
|
||||
}
|
||||
if s != "" && s != "http" && s != "https" {
|
||||
return fmt.Errorf("unknown Scheme")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *ConsulKVRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
s, a, k := obj.inputParser()
|
||||
|
||||
obj.config = api.DefaultConfig()
|
||||
if s != "" {
|
||||
obj.config.Scheme = s
|
||||
}
|
||||
if a != "" {
|
||||
obj.config.Address = obj.Address
|
||||
}
|
||||
obj.key = k // store the key
|
||||
obj.init.Logf("using consul key: %s", obj.key)
|
||||
|
||||
if obj.Token != "" {
|
||||
obj.config.Token = obj.Token
|
||||
}
|
||||
|
||||
var err error
|
||||
obj.client, err = api.NewClient(obj.config)
|
||||
return errwrap.Wrapf(err, "could not create Consul client")
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *ConsulKVRes) Close() error {
|
||||
if obj.config != nil && obj.config.Transport != nil {
|
||||
obj.config.Transport.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the listener and main loop for this resource and it outputs events.
|
||||
func (obj *ConsulKVRes) Watch() error {
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
ch := make(chan error)
|
||||
exit := make(chan struct{})
|
||||
|
||||
kv := obj.client.KV()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer close(ch)
|
||||
defer wg.Done()
|
||||
|
||||
opts := &api.QueryOptions{RequireConsistent: true}
|
||||
ctx, cancel := util.ContextWithCloser(context.Background(), exit)
|
||||
defer cancel()
|
||||
opts = opts.WithContext(ctx)
|
||||
|
||||
for {
|
||||
_, meta, err := kv.Get(obj.key, opts)
|
||||
select {
|
||||
case ch <- err: // send
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// WaitIndex = 0, which means that it is the
|
||||
// first time we run the query, as we are about
|
||||
// to change the WaitIndex to make a blocking
|
||||
// query, we can consider the watch started.
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
if opts.WaitIndex != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if !obj.once {
|
||||
obj.init.Running()
|
||||
obj.once = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Unexpected situation, bug in consul API...
|
||||
select {
|
||||
case ch <- fmt.Errorf("unexpected behaviour in Consul API"):
|
||||
case <-obj.init.Done: // signal for shutdown request
|
||||
}
|
||||
|
||||
case <-obj.init.Done: // signal for shutdown request
|
||||
}
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
defer close(exit)
|
||||
for {
|
||||
select {
|
||||
case err, ok := <-ch:
|
||||
if !ok { // channel shutdown
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("event!")
|
||||
}
|
||||
obj.init.Event()
|
||||
|
||||
case <-obj.init.Done: // signal for shutdown request
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *ConsulKVRes) CheckApply(apply bool) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("consul key: %s", obj.key)
|
||||
}
|
||||
kv := obj.client.KV()
|
||||
pair, _, err := kv.Get(obj.key, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if pair != nil && string(pair.Value) == obj.Value {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
p := &api.KVPair{Key: obj.key, Value: []byte(obj.Value)}
|
||||
_, err = kv.Put(p, nil)
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Cmp compares two resources and return if they are equivalent.
|
||||
func (obj *ConsulKVRes) Cmp(r engine.Res) error {
|
||||
res, ok := r.(*ConsulKVRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Key != res.Key {
|
||||
return fmt.Errorf("the Key param differs")
|
||||
}
|
||||
if obj.Value != res.Value {
|
||||
return fmt.Errorf("the Value param differs")
|
||||
}
|
||||
if obj.Scheme != res.Scheme {
|
||||
return fmt.Errorf("the Scheme param differs")
|
||||
}
|
||||
if obj.Address != res.Address {
|
||||
return fmt.Errorf("the Address param differs")
|
||||
}
|
||||
if obj.Token != res.Token {
|
||||
return fmt.Errorf("the Token param differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// inputParser parses the Name() of a resource and extracts the scheme, address,
|
||||
// and key name of a consul key. We don't have an error, because if we have one,
|
||||
// then it means the input must be a raw key. Output of this function is scheme,
|
||||
// address (includes hostname and port), and key. This also takes our parameters
|
||||
// in to account, and applies the correct overrides if they are specified there.
|
||||
func (obj *ConsulKVRes) inputParser() (string, string, string) {
|
||||
// If the key is specified explicitly, then we're not going to parse the
|
||||
// resource name for a pattern, and we use our given params as they are.
|
||||
if obj.Key != "" {
|
||||
return obj.Scheme, obj.Address, obj.Key
|
||||
}
|
||||
|
||||
// Now we parse...
|
||||
u, err := url.Parse(obj.Name())
|
||||
if err != nil {
|
||||
// If this didn't work, then we know it's explicitly a raw key.
|
||||
return obj.Scheme, obj.Address, obj.Name()
|
||||
}
|
||||
|
||||
// Otherwise, we use the parse result, and we overwrite any of the
|
||||
// fields if we have an explicit param that was specified.
|
||||
k := u.Path
|
||||
s := u.Scheme
|
||||
a := u.Host
|
||||
|
||||
//if obj.Key != "" { // this is now guaranteed to never happen
|
||||
// k = obj.Key
|
||||
//}
|
||||
if obj.Scheme != "" {
|
||||
s = obj.Scheme
|
||||
}
|
||||
if obj.Address != "" {
|
||||
a = obj.Address
|
||||
}
|
||||
|
||||
return s, a, k
|
||||
}
|
||||
71
engine/resources/consul_kv_test.go
Normal file
71
engine/resources/consul_kv_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
)
|
||||
|
||||
func createConsulRes(name string) *ConsulKVRes {
|
||||
r, err := engine.NewNamedResource("consul:kv", name)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("could not create resource: %+v", err))
|
||||
}
|
||||
|
||||
res := r.(*ConsulKVRes) // if this panics, the test will panic
|
||||
return res
|
||||
}
|
||||
|
||||
func TestParseConsulName(t *testing.T) {
|
||||
n1 := "test"
|
||||
r1 := createConsulRes(n1)
|
||||
if s, a, k := r1.inputParser(); s != "" || a != "" || k != "test" {
|
||||
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n1, s, a, k)
|
||||
}
|
||||
|
||||
n2 := "http://127.0.0.1:8500/test"
|
||||
r2 := createConsulRes(n2)
|
||||
if s, a, k := r2.inputParser(); s != "http" || a != "127.0.0.1:8500" || k != "/test" {
|
||||
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n2, s, a, k)
|
||||
}
|
||||
|
||||
n3 := "http://127.0.0.1:8500/test"
|
||||
r3 := createConsulRes(n3)
|
||||
r3.Scheme = "https"
|
||||
r3.Address = "example.com"
|
||||
if s, a, k := r3.inputParser(); s != "https" || a != "example.com" || k != "/test" {
|
||||
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n3, s, a, k)
|
||||
}
|
||||
|
||||
n4 := "http:://127.0.0.1..5:8500/test" // wtf, url.Parse is on drugs...
|
||||
r4 := createConsulRes(n4)
|
||||
//if s, a, k := r4.inputParser(); s != "" || a != "" || k != n4 { // what i really expect
|
||||
if s, a, k := r4.inputParser(); s != "http" || a != "" || k != "" { // what i get
|
||||
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n4, s, a, k)
|
||||
}
|
||||
|
||||
n5 := "http://127.0.0.1:8500/test" // whatever, it's ignored
|
||||
r5 := createConsulRes(n3)
|
||||
r5.Key = "some key"
|
||||
if s, a, k := r5.inputParser(); s != "" || a != "" || k != "some key" {
|
||||
t.Errorf("unexpected output while parsing `%s`: %s, %s, %s", n5, s, a, k)
|
||||
}
|
||||
}
|
||||
559
engine/resources/cron.go
Normal file
559
engine/resources/cron.go
Normal file
@@ -0,0 +1,559 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/user"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
sdbus "github.com/coreos/go-systemd/dbus"
|
||||
"github.com/coreos/go-systemd/unit"
|
||||
systemdUtil "github.com/coreos/go-systemd/util"
|
||||
"github.com/godbus/dbus"
|
||||
)
|
||||
|
||||
const (
|
||||
// OnCalendar is a systemd-timer trigger, whose behaviour is defined in
|
||||
// 'man systemd-timer', and whose format is defined in the 'Calendar
|
||||
// Events' section of 'man systemd-time'.
|
||||
OnCalendar = "OnCalendar"
|
||||
// OnActiveSec is a systemd-timer trigger, whose behaviour is defined in
|
||||
// 'man systemd-timer', and whose format is a time span as defined in
|
||||
// 'man systemd-time'.
|
||||
OnActiveSec = "OnActiveSec"
|
||||
// OnBootSec is a systemd-timer trigger, whose behaviour is defined in
|
||||
// 'man systemd-timer', and whose format is a time span as defined in
|
||||
// 'man systemd-time'.
|
||||
OnBootSec = "OnBootSec"
|
||||
// OnStartupSec is a systemd-timer trigger, whose behaviour is defined in
|
||||
// 'man systemd-timer', and whose format is a time span as defined in
|
||||
// 'man systemd-time'.
|
||||
OnStartupSec = "OnStartupSec"
|
||||
// OnUnitActiveSec is a systemd-timer trigger, whose behaviour is defined
|
||||
// in 'man systemd-timer', and whose format is a time span as defined in
|
||||
// 'man systemd-time'.
|
||||
OnUnitActiveSec = "OnUnitActiveSec"
|
||||
// OnUnitInactiveSec is a systemd-timer trigger, whose behaviour is defined
|
||||
// in 'man systemd-timer', and whose format is a time span as defined in
|
||||
// 'man systemd-time'.
|
||||
OnUnitInactiveSec = "OnUnitInactiveSec"
|
||||
|
||||
// ctxTimeout is the delay, in seconds, before the calls to restart or stop
|
||||
// the systemd unit will error due to timeout.
|
||||
ctxTimeout = 30
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("cron", func() engine.Res { return &CronRes{} })
|
||||
}
|
||||
|
||||
// CronRes is a systemd-timer cron resource.
|
||||
type CronRes struct {
|
||||
traits.Base
|
||||
traits.Edgeable
|
||||
traits.Recvable
|
||||
traits.Refreshable // needed because we embed a svc res
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Unit is the name of the systemd service unit. It is only necessary to
|
||||
// set if you want to specify a service with a different name than the
|
||||
// resource.
|
||||
Unit string `yaml:"unit"`
|
||||
// State must be 'exists' or 'absent'.
|
||||
State string `yaml:"state"`
|
||||
|
||||
// Session, if true, creates the timer as the current user, rather than
|
||||
// root. The service it points to must also be a user unit. It defaults to
|
||||
// false.
|
||||
Session bool `yaml:"session"`
|
||||
|
||||
// Trigger is the type of timer. Valid types are 'OnCalendar',
|
||||
// 'OnActiveSec'. 'OnBootSec'. 'OnStartupSec'. 'OnUnitActiveSec', and
|
||||
// 'OnUnitInactiveSec'. For more information see 'man systemd.timer'.
|
||||
Trigger string `yaml:"trigger"`
|
||||
// Time must be used with all triggers. For 'OnCalendar', it must be in
|
||||
// the format defined in 'man systemd-time' under the heading 'Calendar
|
||||
// Events'. For all other triggers, time should be a valid time span as
|
||||
// defined in 'man systemd-time'
|
||||
Time string `yaml:"time"`
|
||||
|
||||
// AccuracySec is the accuracy of the timer in systemd-time time span
|
||||
// format. It defaults to one minute.
|
||||
AccuracySec string `yaml:"accuracysec"`
|
||||
// RandomizedDelaySec delays the timer by a randomly selected, evenly
|
||||
// distributed amount of time between 0 and the specified time value. The
|
||||
// value must be a valid systemd-time time span.
|
||||
RandomizedDelaySec string `yaml:"randomizeddelaysec"`
|
||||
|
||||
// Persistent, if true, means the time when the service unit was last
|
||||
// triggered is stored on disk. When the timer is activated, the service
|
||||
// unit is triggered immediately if it would have been triggered at least
|
||||
// once during the time when the timer was inactive. It defaults to false.
|
||||
Persistent bool `yaml:"persistent"`
|
||||
// WakeSystem, if true, will cause the system to resume from suspend,
|
||||
// should it be suspended and if the system supports this. It defaults to
|
||||
// false.
|
||||
WakeSystem bool `yaml:"wakesystem"`
|
||||
// RemainAfterElapse, if true, means an elapsed timer will stay loaded, and
|
||||
// its state remains queriable. If false, an elapsed timer unit that cannot
|
||||
// elapse anymore is unloaded. It defaults to true.
|
||||
RemainAfterElapse bool `yaml:"remainafterelapse"`
|
||||
|
||||
file *FileRes // nested file resource
|
||||
recWatcher *recwatch.RecWatcher // recwatcher for nested file
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *CronRes) Default() engine.Res {
|
||||
return &CronRes{
|
||||
State: "exists",
|
||||
RemainAfterElapse: true,
|
||||
}
|
||||
}
|
||||
|
||||
// makeComposite creates a pointer to a FileRes. The pointer is used to validate
|
||||
// and initialize the nested file resource and to apply the file state in
|
||||
// CheckApply.
|
||||
func (obj *CronRes) makeComposite() (*FileRes, error) {
|
||||
p, err := obj.UnitFilePath()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error generating unit file path")
|
||||
}
|
||||
res, err := engine.NewNamedResource("file", p)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error creating nested file resource")
|
||||
}
|
||||
file, ok := res.(*FileRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error casting fileres")
|
||||
}
|
||||
file.State = obj.State
|
||||
if obj.State != "absent" {
|
||||
s := obj.unitFileContents()
|
||||
file.Content = &s
|
||||
}
|
||||
return file, nil
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *CronRes) Validate() error {
|
||||
// validate state
|
||||
if obj.State != "absent" && obj.State != "exists" {
|
||||
return fmt.Errorf("state must be 'absent' or 'exists'")
|
||||
}
|
||||
|
||||
// validate trigger
|
||||
if obj.State == "absent" && obj.Trigger == "" {
|
||||
return nil // if trigger is undefined we can't make a unit file
|
||||
}
|
||||
if obj.Trigger == "" || obj.Time == "" {
|
||||
return fmt.Errorf("trigger and must be set together")
|
||||
}
|
||||
if obj.Trigger != OnCalendar &&
|
||||
obj.Trigger != OnActiveSec &&
|
||||
obj.Trigger != OnBootSec &&
|
||||
obj.Trigger != OnStartupSec &&
|
||||
obj.Trigger != OnUnitActiveSec &&
|
||||
obj.Trigger != OnUnitInactiveSec {
|
||||
|
||||
return fmt.Errorf("invalid trigger")
|
||||
}
|
||||
|
||||
// TODO: Validate time (regex?)
|
||||
|
||||
// validate nested file
|
||||
file, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||
}
|
||||
if err := file.Validate(); err != nil { // composite resource
|
||||
return errwrap.Wrapf(err, "validate failed for embedded file: %s", obj.file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *CronRes) Init(init *engine.Init) error {
|
||||
var err error
|
||||
obj.init = init // save for later
|
||||
|
||||
obj.file, err = obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in init")
|
||||
}
|
||||
return obj.file.Init(init)
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *CronRes) Close() error {
|
||||
if obj.file != nil {
|
||||
return obj.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch for state changes and sends a message to the bus if there is a change.
|
||||
func (obj *CronRes) Watch() error {
|
||||
var bus *dbus.Conn
|
||||
var err error
|
||||
|
||||
// this resource depends on systemd
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return fmt.Errorf("systemd is not running")
|
||||
}
|
||||
|
||||
// create a private message bus
|
||||
if obj.Session {
|
||||
bus, err = util.SessionBusPrivateUsable()
|
||||
} else {
|
||||
bus, err = util.SystemBusPrivateUsable()
|
||||
}
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "failed to connect to bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
// dbus addmatch arguments for the timer unit
|
||||
args := []string{}
|
||||
args = append(args, "type='signal'")
|
||||
args = append(args, "interface='org.freedesktop.systemd1.Manager'")
|
||||
args = append(args, "eavesdrop='true'")
|
||||
args = append(args, fmt.Sprintf("arg2='%s.timer'", obj.Name()))
|
||||
|
||||
// match dbus messsages
|
||||
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, strings.Join(args, ",")); call.Err != nil {
|
||||
return err
|
||||
}
|
||||
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||
|
||||
// channels for dbus signal
|
||||
dbusChan := make(chan *dbus.Signal)
|
||||
defer close(dbusChan)
|
||||
bus.Signal(dbusChan)
|
||||
defer bus.RemoveSignal(dbusChan) // not needed here, but nice for symmetry
|
||||
|
||||
p, err := obj.UnitFilePath()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error generating unit file path")
|
||||
}
|
||||
// recwatcher for the systemd-timer unit file
|
||||
obj.recWatcher, err = recwatch.NewRecWatcher(p, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case event := <-dbusChan:
|
||||
// process dbus events
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("%+v", event)
|
||||
}
|
||||
send = true
|
||||
|
||||
case event, ok := <-obj.recWatcher.Events():
|
||||
// process unit file recwatch events
|
||||
if !ok { // channel shutdown
|
||||
return nil
|
||||
}
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *CronRes) CheckApply(apply bool) (bool, error) {
|
||||
checkOK := true
|
||||
// use the embedded file resource to apply the correct state
|
||||
if c, err := obj.file.CheckApply(apply); err != nil {
|
||||
return false, errwrap.Wrapf(err, "nested file failed")
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
// check timer state and apply the defined state if needed
|
||||
if c, err := obj.unitCheckApply(apply); err != nil {
|
||||
return false, errwrap.Wrapf(err, "unitCheckApply error")
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// unitCheckApply checks the state of the systemd-timer unit and, if apply is
|
||||
// true, applies the defined state.
|
||||
func (obj *CronRes) unitCheckApply(apply bool) (bool, error) {
|
||||
var conn *sdbus.Conn
|
||||
var godbusConn *dbus.Conn
|
||||
var err error
|
||||
|
||||
// this resource depends on systemd to ensure that it's running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return false, fmt.Errorf("systemd is not running")
|
||||
}
|
||||
// go-systemd connection
|
||||
if obj.Session {
|
||||
conn, err = sdbus.NewUserConnection()
|
||||
} else {
|
||||
conn, err = sdbus.New() // system bus
|
||||
}
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error making go-systemd dbus connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// get the load state and active state of the timer unit
|
||||
loadState, err := conn.GetUnitProperty(fmt.Sprintf("%s.timer", obj.Name()), "LoadState")
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to get load state")
|
||||
}
|
||||
activeState, err := conn.GetUnitProperty(fmt.Sprintf("%s.timer", obj.Name()), "ActiveState")
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to get active state")
|
||||
}
|
||||
// check the timer unit state
|
||||
if obj.State == "absent" && loadState.Value == dbus.MakeVariant("not-found") {
|
||||
return true, nil
|
||||
}
|
||||
if obj.State == "exists" && activeState.Value == dbus.MakeVariant("active") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// systemctl daemon-reload
|
||||
if err := conn.Reload(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error reloading daemon")
|
||||
}
|
||||
|
||||
// context for stopping/restarting the unit
|
||||
ctx, cancel := context.WithTimeout(context.Background(), ctxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// godbus connection for stopping/restarting the unit
|
||||
if obj.Session {
|
||||
godbusConn, err = util.SessionBusPrivateUsable()
|
||||
} else {
|
||||
godbusConn, err = util.SystemBusPrivateUsable()
|
||||
}
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error making godbus connection")
|
||||
}
|
||||
defer godbusConn.Close()
|
||||
|
||||
// stop or restart the unit
|
||||
if obj.State == "absent" {
|
||||
return false, engineUtil.StopUnit(ctx, godbusConn, fmt.Sprintf("%s.timer", obj.Name()))
|
||||
}
|
||||
return false, engineUtil.RestartUnit(ctx, godbusConn, fmt.Sprintf("%s.timer", obj.Name()))
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *CronRes) Cmp(r engine.Res) error {
|
||||
res, ok := r.(*CronRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("state differs: %s vs %s", obj.State, res.State)
|
||||
}
|
||||
if obj.Trigger != res.Trigger {
|
||||
return fmt.Errorf("trigger differs: %s vs %s", obj.Trigger, res.Trigger)
|
||||
}
|
||||
if obj.Time != res.Time {
|
||||
return fmt.Errorf("time differs: %s vs %s", obj.Time, res.Time)
|
||||
}
|
||||
if obj.AccuracySec != res.AccuracySec {
|
||||
return fmt.Errorf("accuracysec differs: %s vs %s", obj.AccuracySec, res.AccuracySec)
|
||||
}
|
||||
if obj.RandomizedDelaySec != res.RandomizedDelaySec {
|
||||
return fmt.Errorf("randomizeddelaysec differs: %s vs %s", obj.RandomizedDelaySec, res.RandomizedDelaySec)
|
||||
}
|
||||
if obj.Unit != res.Unit {
|
||||
return fmt.Errorf("unit differs: %s vs %s", obj.Unit, res.Unit)
|
||||
}
|
||||
if obj.Persistent != res.Persistent {
|
||||
return fmt.Errorf("persistent differs: %t vs %t", obj.Persistent, res.Persistent)
|
||||
}
|
||||
if obj.WakeSystem != res.WakeSystem {
|
||||
return fmt.Errorf("wakesystem differs: %t vs %t", obj.WakeSystem, res.WakeSystem)
|
||||
}
|
||||
if obj.RemainAfterElapse != res.RemainAfterElapse {
|
||||
return fmt.Errorf("remainafterelapse differs: %t vs %t", obj.RemainAfterElapse, res.RemainAfterElapse)
|
||||
}
|
||||
return obj.file.Cmp(r)
|
||||
}
|
||||
|
||||
// CronUID is a unique resource identifier.
|
||||
type CronUID struct {
|
||||
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||
// information about where this UID came from, and is unrelated to the
|
||||
// information about the resource we're matching. That data which is
|
||||
// used in the IFF function, is what you see in the struct fields here.
|
||||
engine.BaseUID
|
||||
|
||||
unit string // name of target unit
|
||||
session bool // user session
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *CronUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*CronUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if obj.unit != res.unit {
|
||||
return false
|
||||
}
|
||||
if obj.session != res.session {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// AutoEdges returns the AutoEdge interface.
|
||||
func (obj *CronRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one although some resources can return multiple.
|
||||
func (obj *CronRes) UIDs() []engine.ResUID {
|
||||
unit := fmt.Sprintf("%s.service", obj.Name())
|
||||
if obj.Unit != "" {
|
||||
unit = obj.Unit
|
||||
}
|
||||
uids := []engine.ResUID{
|
||||
&CronUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
unit: unit, // name of target unit
|
||||
session: obj.Session, // user session
|
||||
},
|
||||
}
|
||||
if file, err := obj.makeComposite(); err == nil {
|
||||
uids = append(uids, file.UIDs()...) // add the file uid if we can
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes CronRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*CronRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to CronRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = CronRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnitFilePath returns the path to the systemd-timer unit file.
|
||||
func (obj *CronRes) UnitFilePath() (string, error) {
|
||||
// root timer
|
||||
if !obj.Session {
|
||||
return fmt.Sprintf("/etc/systemd/system/%s.timer", obj.Name()), nil
|
||||
}
|
||||
// user timer
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf(err, "error getting current user")
|
||||
}
|
||||
if u.HomeDir == "" {
|
||||
return "", fmt.Errorf("user has no home directory")
|
||||
}
|
||||
return path.Join(u.HomeDir, "/.config/systemd/user/", fmt.Sprintf("%s.timer", obj.Name())), nil
|
||||
}
|
||||
|
||||
// unitFileContents returns the contents of the unit file representing the
|
||||
// CronRes struct.
|
||||
func (obj *CronRes) unitFileContents() string {
|
||||
u := []*unit.UnitOption{}
|
||||
|
||||
// [Unit]
|
||||
u = append(u, &unit.UnitOption{Section: "Unit", Name: "Description", Value: "timer generated by mgmt"})
|
||||
// [Timer]
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: obj.Trigger, Value: obj.Time})
|
||||
if obj.AccuracySec != "" {
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "AccuracySec", Value: obj.AccuracySec})
|
||||
}
|
||||
if obj.RandomizedDelaySec != "" {
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "RandomizedDelaySec", Value: obj.RandomizedDelaySec})
|
||||
}
|
||||
if obj.Unit != "" {
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "Unit", Value: obj.Unit})
|
||||
}
|
||||
if obj.Persistent != false { // defaults to false
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "Persistent", Value: "true"})
|
||||
}
|
||||
if obj.WakeSystem != false { // defaults to false
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "WakeSystem", Value: "true"})
|
||||
}
|
||||
if obj.RemainAfterElapse != true { // defaults to true
|
||||
u = append(u, &unit.UnitOption{Section: "Timer", Name: "RemainAfterElapse", Value: "false"})
|
||||
}
|
||||
// [Install]
|
||||
u = append(u, &unit.UnitOption{Section: "Install", Name: "WantedBy", Value: "timers.target"})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(unit.Serialize(u))
|
||||
return buf.String()
|
||||
}
|
||||
1177
engine/resources/dhcp.go
Normal file
1177
engine/resources/dhcp.go
Normal file
File diff suppressed because it is too large
Load Diff
493
engine/resources/docker_container.go
Normal file
493
engine/resources/docker_container.go
Normal file
@@ -0,0 +1,493 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !nodocker
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContainerRunning is the running container state.
|
||||
ContainerRunning = "running"
|
||||
// ContainerStopped is the stopped container state.
|
||||
ContainerStopped = "stopped"
|
||||
// ContainerRemoved is the removed container state.
|
||||
ContainerRemoved = "removed"
|
||||
|
||||
// initCtxTimeout is the length of time, in seconds, before requests are
|
||||
// cancelled in Init.
|
||||
initCtxTimeout = 20
|
||||
// checkApplyCtxTimeout is the length of time, in seconds, before
|
||||
// requests are cancelled in CheckApply.
|
||||
checkApplyCtxTimeout = 120
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("docker:container", func() engine.Res { return &DockerContainerRes{} })
|
||||
}
|
||||
|
||||
// DockerContainerRes is a docker container resource.
|
||||
type DockerContainerRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
|
||||
// State of the container must be running, stopped, or removed.
|
||||
State string `yaml:"state"`
|
||||
// Image is a docker image, or image:tag.
|
||||
Image string `yaml:"image"`
|
||||
// Cmd is a command, or list of commands to run on the container.
|
||||
Cmd []string `yaml:"cmd"`
|
||||
// Env is a list of environment variables. E.g. ["VAR=val",].
|
||||
Env []string `yaml:"env"`
|
||||
// Ports is a map of port bindings. E.g. {"tcp" => {80 => 8080},}.
|
||||
Ports map[string]map[int64]int64 `yaml:"ports"`
|
||||
// APIVersion allows you to override the host's default client API
|
||||
// version.
|
||||
APIVersion string `yaml:"apiversion"`
|
||||
|
||||
// Force, if true, this will destroy and redeploy the container if the
|
||||
// image is incorrect.
|
||||
Force bool `yaml:"force"`
|
||||
|
||||
client *client.Client // docker api client
|
||||
|
||||
init *engine.Init
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *DockerContainerRes) Default() engine.Res {
|
||||
return &DockerContainerRes{
|
||||
State: "running",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *DockerContainerRes) Validate() error {
|
||||
// validate state
|
||||
if obj.State != ContainerRunning && obj.State != ContainerStopped && obj.State != ContainerRemoved {
|
||||
return fmt.Errorf("state must be running, stopped or removed")
|
||||
}
|
||||
|
||||
// make sure an image is specified
|
||||
if obj.Image == "" {
|
||||
return fmt.Errorf("image must be specified")
|
||||
}
|
||||
|
||||
// validate env
|
||||
for _, env := range obj.Env {
|
||||
if !strings.Contains(env, "=") || strings.Contains(env, " ") {
|
||||
return fmt.Errorf("invalid environment variable: %s", env)
|
||||
}
|
||||
}
|
||||
|
||||
// validate ports
|
||||
for k, v := range obj.Ports {
|
||||
if k != "tcp" && k != "udp" && k != "sctp" {
|
||||
return fmt.Errorf("ports primary key should be tcp, udp or sctp")
|
||||
}
|
||||
for p, q := range v {
|
||||
if (p < 1 || p > 65535) || (q < 1 || q > 65535) {
|
||||
return fmt.Errorf("ports must be between 1 and 65535")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate APIVersion
|
||||
if obj.APIVersion != "" {
|
||||
verOK, err := regexp.MatchString(`^(v)[1-9]\.[0-9]\d*$`, obj.APIVersion)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error matching apiversion string")
|
||||
}
|
||||
if !verOK {
|
||||
return fmt.Errorf("invalid apiversion: %s", obj.APIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *DockerContainerRes) Init(init *engine.Init) error {
|
||||
var err error
|
||||
obj.init = init // save for later
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), initCtxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Initialize the docker client.
|
||||
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error creating docker client")
|
||||
}
|
||||
|
||||
// Validate the image.
|
||||
resp, err := obj.client.ImageSearch(ctx, obj.Image, types.ImageSearchOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error searching for image")
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return fmt.Errorf("image: %s not found", obj.Image)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *DockerContainerRes) Close() error {
|
||||
return obj.client.Close() // close the docker client
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *DockerContainerRes) Watch() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
eventChan, errChan := obj.client.Events(ctx, types.EventsOptions{})
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventChan:
|
||||
if !ok { // channel shutdown
|
||||
return nil
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("%+v", event)
|
||||
}
|
||||
send = true
|
||||
|
||||
case err, ok := <-errChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Docker resource.
|
||||
func (obj *DockerContainerRes) CheckApply(apply bool) (bool, error) {
|
||||
var id string
|
||||
var destroy bool
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), checkApplyCtxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// List any container whose name matches this resource.
|
||||
opts := types.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: obj.Name()}),
|
||||
}
|
||||
containerList, err := obj.client.ContainerList(ctx, opts)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error listing containers")
|
||||
}
|
||||
|
||||
if len(containerList) > 1 {
|
||||
return false, fmt.Errorf("more than one container named %s", obj.Name())
|
||||
}
|
||||
if len(containerList) == 0 && obj.State == ContainerRemoved {
|
||||
return true, nil
|
||||
}
|
||||
if len(containerList) == 1 {
|
||||
// If the state and image are correct, we're done.
|
||||
if containerList[0].State == obj.State && containerList[0].Image == obj.Image {
|
||||
return true, nil
|
||||
}
|
||||
id = containerList[0].ID // save the id for later
|
||||
// If the image is wrong, and force is true, mark the container for
|
||||
// destruction.
|
||||
if containerList[0].Image != obj.Image && obj.Force {
|
||||
destroy = true
|
||||
}
|
||||
// Otherwise return an error.
|
||||
if containerList[0].Image != obj.Image && !obj.Force {
|
||||
return false, fmt.Errorf("%s exists but has the wrong image: %s", obj.Name(), containerList[0].Image)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if obj.State == ContainerStopped { // container exists and should be stopped
|
||||
return false, obj.containerStop(ctx, id, nil)
|
||||
}
|
||||
|
||||
if obj.State == ContainerRemoved { // container exists and should be removed
|
||||
if err := obj.containerStop(ctx, id, nil); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, obj.containerRemove(ctx, id, types.ContainerRemoveOptions{})
|
||||
}
|
||||
|
||||
if destroy {
|
||||
if err := obj.containerStop(ctx, id, nil); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := obj.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
containerList = []types.Container{} // zero the list
|
||||
}
|
||||
|
||||
if len(containerList) == 0 { // no container was found
|
||||
// Download the specified image if it doesn't exist locally.
|
||||
p, err := obj.client.ImagePull(ctx, obj.Image, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error pulling image")
|
||||
}
|
||||
// Wait for the image to download, EOF signals that it's done.
|
||||
if _, err := ioutil.ReadAll(p); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error reading image pull result")
|
||||
}
|
||||
|
||||
// set up port bindings
|
||||
containerConfig := &container.Config{
|
||||
Image: obj.Image,
|
||||
Cmd: obj.Cmd,
|
||||
Env: obj.Env,
|
||||
ExposedPorts: make(map[nat.Port]struct{}),
|
||||
}
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: make(map[nat.Port][]nat.PortBinding),
|
||||
}
|
||||
|
||||
for k, v := range obj.Ports {
|
||||
for p, q := range v {
|
||||
containerConfig.ExposedPorts[nat.Port(k)] = struct{}{}
|
||||
hostConfig.PortBindings[nat.Port(fmt.Sprintf("%d/%s", p, k))] = []nat.PortBinding{
|
||||
{
|
||||
HostIP: "0.0.0.0",
|
||||
HostPort: fmt.Sprintf("%d", q),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, nil, obj.Name())
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error creating container")
|
||||
}
|
||||
id = c.ID
|
||||
}
|
||||
|
||||
return false, obj.containerStart(ctx, id, types.ContainerStartOptions{})
|
||||
}
|
||||
|
||||
// containerStart starts the specified container, and waits for it to start.
|
||||
func (obj *DockerContainerRes) containerStart(ctx context.Context, id string, opts types.ContainerStartOptions) error {
|
||||
// Get an events channel for the container we're about to start.
|
||||
eventOpts := types.EventsOptions{
|
||||
Filters: filters.NewArgs(filters.KeyValuePair{Key: "container", Value: id}),
|
||||
}
|
||||
eventCh, errCh := obj.client.Events(ctx, eventOpts)
|
||||
// Start the container.
|
||||
if err := obj.client.ContainerStart(ctx, id, opts); err != nil {
|
||||
return errwrap.Wrapf(err, "error starting container")
|
||||
}
|
||||
// Wait for a message on eventChan that says the container has started.
|
||||
select {
|
||||
case event := <-eventCh:
|
||||
if event.Status != "start" {
|
||||
return fmt.Errorf("unexpected event: %+v", event)
|
||||
}
|
||||
case err := <-errCh:
|
||||
return errwrap.Wrapf(err, "error waiting for container start")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// containerStop stops the specified container and waits for it to stop.
|
||||
func (obj *DockerContainerRes) containerStop(ctx context.Context, id string, timeout *time.Duration) error {
|
||||
ch, errCh := obj.client.ContainerWait(ctx, id, container.WaitConditionNotRunning)
|
||||
obj.client.ContainerStop(ctx, id, timeout)
|
||||
select {
|
||||
case <-ch:
|
||||
case err := <-errCh:
|
||||
return errwrap.Wrapf(err, "error waiting for container to stop")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// containerRemove removes the specified container and waits for it to be
|
||||
// removed.
|
||||
func (obj *DockerContainerRes) containerRemove(ctx context.Context, id string, opts types.ContainerRemoveOptions) error {
|
||||
ch, errCh := obj.client.ContainerWait(ctx, id, container.WaitConditionRemoved)
|
||||
obj.client.ContainerRemove(ctx, id, opts)
|
||||
select {
|
||||
case <-ch:
|
||||
case err := <-errCh:
|
||||
return errwrap.Wrapf(err, "error waiting for container to be removed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *DockerContainerRes) Cmp(r engine.Res) error {
|
||||
// we can only compare DockerContainerRes to others of the same resource kind
|
||||
res, ok := r.(*DockerContainerRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("error casting r to *DockerContainerRes")
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
if obj.Image != res.Image {
|
||||
return fmt.Errorf("the Image differs")
|
||||
}
|
||||
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
|
||||
return errwrap.Wrapf(err, "the Cmd field differs")
|
||||
}
|
||||
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
|
||||
return errwrap.Wrapf(err, "tne Env field differs")
|
||||
}
|
||||
if len(obj.Ports) != len(res.Ports) {
|
||||
return fmt.Errorf("the Ports length differs")
|
||||
}
|
||||
for k, v := range obj.Ports {
|
||||
for p, q := range v {
|
||||
if w, ok := res.Ports[k][p]; !ok || q != w {
|
||||
return fmt.Errorf("the Ports field differs")
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.APIVersion != res.APIVersion {
|
||||
return fmt.Errorf("the APIVersion differs")
|
||||
}
|
||||
if obj.Force != res.Force {
|
||||
return fmt.Errorf("the Force field differs")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DockerContainerUID is the UID struct for DockerContainerRes.
|
||||
type DockerContainerUID struct {
|
||||
engine.BaseUID
|
||||
|
||||
name string
|
||||
}
|
||||
|
||||
// DockerContainerResAutoEdges holds the state of the auto edge generator.
|
||||
type DockerContainerResAutoEdges struct {
|
||||
UIDs []engine.ResUID
|
||||
pointer int
|
||||
}
|
||||
|
||||
// AutoEdges returns edges to any docker:image resource that matches the image
|
||||
// specified in the docker:container resource definition.
|
||||
func (obj *DockerContainerRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
var result []engine.ResUID
|
||||
var reversed bool
|
||||
if obj.State != "removed" {
|
||||
reversed = true
|
||||
}
|
||||
result = append(result, &DockerImageUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Reversed: &reversed,
|
||||
},
|
||||
image: dockerImageNameTag(obj.Image),
|
||||
})
|
||||
return &DockerContainerResAutoEdges{
|
||||
UIDs: result,
|
||||
pointer: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Next returnes the next automatic edge.
|
||||
func (obj *DockerContainerResAutoEdges) Next() []engine.ResUID {
|
||||
if len(obj.UIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
value := obj.UIDs[obj.pointer]
|
||||
obj.pointer++
|
||||
return []engine.ResUID{value}
|
||||
}
|
||||
|
||||
// Test gets results of the earlier Next() call, & returns if we should
|
||||
// continue.
|
||||
func (obj *DockerContainerResAutoEdges) Test(input []bool) bool {
|
||||
if len(obj.UIDs) <= obj.pointer {
|
||||
return false
|
||||
}
|
||||
if len(input) != 1 { // in case we get given bad data
|
||||
panic(fmt.Sprintf("Expecting a single value!"))
|
||||
}
|
||||
return true // keep going
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *DockerContainerRes) UIDs() []engine.ResUID {
|
||||
x := &DockerContainerUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *DockerContainerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes DockerContainerRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*DockerContainerRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to DockerContainerRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = DockerContainerRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
202
engine/resources/docker_container_test.go
Normal file
202
engine/resources/docker_container_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !nodocker
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
)
|
||||
|
||||
var res *DockerContainerRes
|
||||
|
||||
var id string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
var setupCode, testCode, cleanupCode int
|
||||
|
||||
if err := setup(); err != nil {
|
||||
log.Printf("error during setup: %s", err)
|
||||
setupCode = 1
|
||||
}
|
||||
|
||||
if setupCode == 0 {
|
||||
testCode = m.Run()
|
||||
}
|
||||
|
||||
if err := cleanup(); err != nil {
|
||||
log.Printf("error during cleanup: %s", err)
|
||||
cleanupCode = 1
|
||||
}
|
||||
|
||||
os.Exit(setupCode + testCode + cleanupCode)
|
||||
}
|
||||
|
||||
func Test_containerStart(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := res.containerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
|
||||
t.Errorf("containerStart() error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
l, err := res.client.ContainerList(
|
||||
ctx,
|
||||
types.ContainerListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.KeyValuePair{Key: "id", Value: id},
|
||||
filters.KeyValuePair{Key: "status", Value: "running"},
|
||||
),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("error listing containers: %s", err)
|
||||
return
|
||||
}
|
||||
if len(l) != 1 {
|
||||
t.Errorf("failed to start container")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Test_containerStop(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := res.containerStop(ctx, id, nil); err != nil {
|
||||
t.Errorf("containerStop() error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
l, err := res.client.ContainerList(
|
||||
ctx,
|
||||
types.ContainerListOptions{
|
||||
Filters: filters.NewArgs(
|
||||
filters.KeyValuePair{Key: "id", Value: id},
|
||||
),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("error listing containers: %s", err)
|
||||
return
|
||||
}
|
||||
if len(l) != 0 {
|
||||
t.Errorf("failed to stop container")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func Test_containerRemove(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := res.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
|
||||
t.Errorf("containerRemove() error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
l, err := res.client.ContainerList(
|
||||
ctx,
|
||||
types.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(
|
||||
filters.KeyValuePair{Key: "id", Value: id},
|
||||
),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
t.Errorf("error listing containers: %s", err)
|
||||
return
|
||||
}
|
||||
if len(l) != 0 {
|
||||
t.Errorf("failed to remove container")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func setup() error {
|
||||
var err error
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res = &DockerContainerRes{}
|
||||
res.Init(res.init)
|
||||
|
||||
p, err := res.client.ImagePull(ctx, "alpine", types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error pulling image: %s", err)
|
||||
}
|
||||
if _, err := ioutil.ReadAll(p); err != nil {
|
||||
return fmt.Errorf("error reading image pull result: %s", err)
|
||||
}
|
||||
|
||||
resp, err := res.client.ContainerCreate(
|
||||
ctx,
|
||||
&container.Config{
|
||||
Image: "alpine",
|
||||
Cmd: []string{"sleep", "100"},
|
||||
},
|
||||
&container.HostConfig{},
|
||||
nil,
|
||||
nil,
|
||||
"mgmt-test",
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating container: %s", err)
|
||||
}
|
||||
id = resp.ID
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanup() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
l, err := res.client.ContainerList(
|
||||
ctx,
|
||||
types.ContainerListOptions{
|
||||
All: true,
|
||||
Filters: filters.NewArgs(filters.KeyValuePair{Key: "id", Value: id}),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error listing containers: %s", err)
|
||||
}
|
||||
|
||||
if len(l) > 0 {
|
||||
if err := res.client.ContainerStop(ctx, id, nil); err != nil {
|
||||
return fmt.Errorf("error stopping container: %s", err)
|
||||
}
|
||||
if err := res.client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("error removing container: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
295
engine/resources/docker_image.go
Normal file
295
engine/resources/docker_image.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !nodocker
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/client"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// dockerImageInitCtxTimeout is the length of time, in seconds, before
|
||||
// requests are cancelled in Init.
|
||||
dockerImageInitCtxTimeout = 20
|
||||
// dockerImageCheckApplyCtxTimeout is the length of time, in seconds,
|
||||
// before requests are cancelled in CheckApply.
|
||||
dockerImageCheckApplyCtxTimeout = 120
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("docker:image", func() engine.Res { return &DockerImageRes{} })
|
||||
}
|
||||
|
||||
// DockerImageRes is a docker image resource. The resource's name must be a
|
||||
// docker image in any supported format (url, image, or image:tag).
|
||||
type DockerImageRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
|
||||
// State of the image must be exists or absent.
|
||||
State string `yaml:"state"`
|
||||
// APIVersion allows you to override the host's default client API
|
||||
// version.
|
||||
APIVersion string `yaml:"apiversion"`
|
||||
|
||||
image string // full image:tag format
|
||||
client *client.Client // docker api client
|
||||
|
||||
init *engine.Init
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *DockerImageRes) Default() engine.Res {
|
||||
return &DockerImageRes{
|
||||
// TODO: eventually if image supports other properties, this can
|
||||
// be left out and we could have the state be "unmanaged".
|
||||
State: "exists",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *DockerImageRes) Validate() error {
|
||||
// validate state
|
||||
if obj.State != "exists" && obj.State != "absent" {
|
||||
return fmt.Errorf("state must be exists or absent")
|
||||
}
|
||||
|
||||
// validate APIVersion
|
||||
if obj.APIVersion != "" {
|
||||
verOK, err := regexp.MatchString(`^(v)[1-9]\.[0-9]\d*$`, obj.APIVersion)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error matching apiversion string")
|
||||
}
|
||||
if !verOK {
|
||||
return fmt.Errorf("invalid apiversion: %s", obj.APIVersion)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *DockerImageRes) Init(init *engine.Init) error {
|
||||
var err error
|
||||
obj.init = init // save for later
|
||||
|
||||
// Save the full image name and tag.
|
||||
obj.image = dockerImageNameTag(obj.Name())
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dockerImageInitCtxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Initialize the docker client.
|
||||
obj.client, err = client.NewClientWithOpts(client.WithVersion(obj.APIVersion))
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error creating docker client")
|
||||
}
|
||||
|
||||
// Validate the image.
|
||||
resp, err := obj.client.ImageSearch(ctx, obj.image, types.ImageSearchOptions{Limit: 1})
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error searching for image")
|
||||
}
|
||||
if len(resp) == 0 {
|
||||
return fmt.Errorf("image: %s not found", obj.image)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *DockerImageRes) Close() error {
|
||||
return obj.client.Close() // close the docker client
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *DockerImageRes) Watch() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
eventChan, errChan := obj.client.Events(ctx, types.EventsOptions{})
|
||||
|
||||
// notify engine that we're running
|
||||
obj.init.Running()
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-eventChan:
|
||||
if !ok { // channel shutdown
|
||||
return nil
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("%+v", event)
|
||||
}
|
||||
send = true
|
||||
|
||||
case err, ok := <-errChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Docker resource.
|
||||
func (obj *DockerImageRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), dockerImageCheckApplyCtxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
s, err := obj.client.ImageList(ctx, types.ImageListOptions{
|
||||
Filters: filters.NewArgs(filters.Arg("reference", obj.image)),
|
||||
})
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error listing images")
|
||||
}
|
||||
if len(s) > 1 {
|
||||
return false, fmt.Errorf("more than one image found")
|
||||
}
|
||||
|
||||
if obj.State == "absent" && len(s) == 0 {
|
||||
return true, nil
|
||||
}
|
||||
if obj.State == "exists" && len(s) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if obj.State == "absent" {
|
||||
// TODO: force? prune children?
|
||||
if _, err := obj.client.ImageRemove(ctx, obj.image, types.ImageRemoveOptions{}); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error removing image")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// pull the image
|
||||
p, err := obj.client.ImagePull(ctx, obj.image, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error pulling image")
|
||||
}
|
||||
// Wait for the image to download, EOF signals that it's done.
|
||||
if _, err := ioutil.ReadAll(p); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error reading image pull result")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *DockerImageRes) Cmp(r engine.Res) error {
|
||||
// we can only compare DockerImageRes to others of the same resource kind
|
||||
res, ok := r.(*DockerImageRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("error casting r to *DockerImageRes")
|
||||
}
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
|
||||
if obj.APIVersion != res.APIVersion {
|
||||
return fmt.Errorf("the APIVersion differs")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DockerImageUID is the UID struct for DockerImageRes.
|
||||
type DockerImageUID struct {
|
||||
engine.BaseUID
|
||||
|
||||
image string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *DockerImageRes) UIDs() []engine.ResUID {
|
||||
x := &DockerImageUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
image: dockerImageNameTag(obj.Name()),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// AutoEdges returns the AutoEdge interface.
|
||||
func (obj *DockerImageRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *DockerImageUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*DockerImageUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.image == res.image
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *DockerImageRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes DockerImageRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*DockerImageRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to DockerImageRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = DockerImageRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// dockerImageNameTag does a naive check to see if the input includes a tag or
|
||||
// is a url, and if not, appends the `:latest` tag to ensure disambiguation.
|
||||
func dockerImageNameTag(image string) string {
|
||||
if strings.Contains(image, ":") {
|
||||
return image
|
||||
}
|
||||
return image + ":latest"
|
||||
}
|
||||
887
engine/resources/exec.go
Normal file
887
engine/resources/exec.go
Normal file
@@ -0,0 +1,887 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("exec", func() engine.Res { return &ExecRes{} })
|
||||
}
|
||||
|
||||
// ExecRes is an exec resource for running commands.
|
||||
type ExecRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
traits.Sendable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Cmd is the command to run. If this is not specified, we use the name.
|
||||
Cmd string `yaml:"cmd"`
|
||||
// Args is a list of args to pass to Cmd. This can be used *instead* of
|
||||
// passing the full command and args as a single string to Cmd. It can
|
||||
// only be used when a Shell is *not* specified. The advantage of this
|
||||
// is that you don't have to worry about escape characters.
|
||||
Args []string `yaml:"args"`
|
||||
// Cwd is the dir to run the command in. If empty, then this will use
|
||||
// the working directory of the calling process. (This process is mgmt,
|
||||
// not the process being run here.)
|
||||
Cwd string `yaml:"cwd"`
|
||||
// Shell is the (optional) shell to use to run the cmd. If you specify
|
||||
// this, then you can't use the Args parameter.
|
||||
Shell string `yaml:"shell"`
|
||||
// Timeout is the number of seconds to wait before sending a Kill to the
|
||||
// running command. If the Kill is received before the process exits,
|
||||
// then this be treated as an error.
|
||||
Timeout uint64 `yaml:"timeout"`
|
||||
// Env allows the user to specify environment variables for script
|
||||
// execution. These are taken using a map of format of VAR_NAME -> value.
|
||||
Env map[string]string `yaml:"env"`
|
||||
|
||||
// Watch is the command to run to detect event changes. Each line of
|
||||
// output from this command is treated as an event.
|
||||
WatchCmd string `yaml:"watchcmd"`
|
||||
// WatchCwd is the Cwd for the WatchCmd. See the docs for Cwd.
|
||||
WatchCwd string `yaml:"watchcwd"`
|
||||
// WatchShell is the Shell for the WatchCmd. See the docs for Shell.
|
||||
WatchShell string `yaml:"watchshell"`
|
||||
|
||||
// IfCmd is the command that runs to guard against running the Cmd. If
|
||||
// this command succeeds, then Cmd *will* be run. If this command
|
||||
// returns a non-zero result, then the Cmd will not be run. Any error
|
||||
// scenario or timeout will cause the resource to error.
|
||||
IfCmd string `yaml:"ifcmd"`
|
||||
// IfCwd is the Cwd for the IfCmd. See the docs for Cwd.
|
||||
IfCwd string `yaml:"ifcwd"`
|
||||
// IfShell is the Shell for the IfCmd. See the docs for Shell.
|
||||
IfShell string `yaml:"ifshell"`
|
||||
|
||||
// User is the (optional) user to use to execute the command. It is used
|
||||
// for any command being run.
|
||||
User string `yaml:"user"`
|
||||
// Group is the (optional) group to use to execute the command. It is
|
||||
// used for any command being run.
|
||||
Group string `yaml:"group"`
|
||||
|
||||
output *string // all cmd output, read only, do not set!
|
||||
stdout *string // the cmd stdout, read only, do not set!
|
||||
stderr *string // the cmd stderr, read only, do not set!
|
||||
|
||||
interruptChan chan struct{}
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *ExecRes) Default() engine.Res {
|
||||
return &ExecRes{}
|
||||
}
|
||||
|
||||
// getCmd returns the actual command to run. When Cmd is not specified, we use
|
||||
// the Name.
|
||||
func (obj *ExecRes) getCmd() string {
|
||||
if obj.Cmd != "" {
|
||||
return obj.Cmd
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *ExecRes) Validate() error {
|
||||
if obj.getCmd() == "" { // this is the only thing that is really required
|
||||
return fmt.Errorf("the Cmd can't be empty")
|
||||
}
|
||||
|
||||
split := strings.Fields(obj.getCmd())
|
||||
if len(obj.Args) > 0 && obj.Shell != "" {
|
||||
return fmt.Errorf("the Args param can't be used with a Shell")
|
||||
}
|
||||
if len(obj.Args) > 0 && len(split) > 1 {
|
||||
return fmt.Errorf("the Args param can't be used when Cmd has args")
|
||||
}
|
||||
|
||||
// check that, if an user or a group is set, we're running as root
|
||||
if obj.User != "" || obj.Group != "" {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error looking up current user")
|
||||
}
|
||||
if currentUser.Uid != "0" {
|
||||
return fmt.Errorf("running as root is required if you want to use exec with a different user/group")
|
||||
}
|
||||
}
|
||||
|
||||
// check that environment variables' format is valid
|
||||
for key := range obj.Env {
|
||||
if err := isNameValid(key); err != nil {
|
||||
return errwrap.Wrapf(err, "invalid variable name")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *ExecRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
obj.interruptChan = make(chan struct{})
|
||||
obj.wg = &sync.WaitGroup{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *ExecRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *ExecRes) Watch() error {
|
||||
ioChan := make(chan *cmdOutput)
|
||||
defer obj.wg.Wait()
|
||||
|
||||
if obj.WatchCmd != "" {
|
||||
var cmdName string
|
||||
var cmdArgs []string
|
||||
if obj.WatchShell == "" {
|
||||
// call without a shell
|
||||
// FIXME: are there still whitespace splitting issues?
|
||||
split := strings.Fields(obj.WatchCmd)
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:]
|
||||
} else {
|
||||
cmdName = obj.WatchShell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.WatchCmd}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||
cmd.Dir = obj.WatchCwd // run program in pwd if ""
|
||||
// ignore signals sent to parent process (we're in our own group)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
// if we have a user and group, use them
|
||||
var err error
|
||||
if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
|
||||
return errwrap.Wrapf(err, "error while setting credential")
|
||||
}
|
||||
|
||||
if ioChan, err = obj.cmdOutputRunner(ctx, cmd); err != nil {
|
||||
return errwrap.Wrapf(err, "error starting WatchCmd")
|
||||
}
|
||||
}
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case data, ok := <-ioChan:
|
||||
if !ok { // EOF
|
||||
// FIXME: add an "if watch command ends/crashes"
|
||||
// restart or generate error option
|
||||
return fmt.Errorf("reached EOF")
|
||||
}
|
||||
if err := data.err; err != nil {
|
||||
// error reading input or cmd failure
|
||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||
if !ok {
|
||||
// command failed in some bad way
|
||||
return errwrap.Wrapf(err, "watchcmd failed in some bad way")
|
||||
}
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return errwrap.Wrapf(err, "could not get exit status of watchcmd")
|
||||
}
|
||||
exitStatus := wStatus.ExitStatus()
|
||||
if exitStatus == 0 {
|
||||
// i'm not sure if this could happen
|
||||
return errwrap.Wrapf(err, "unexpected watchcmd exit status of zero")
|
||||
}
|
||||
|
||||
obj.init.Logf("watchcmd exited with: %d", exitStatus)
|
||||
return errwrap.Wrapf(err, "watchcmd errored")
|
||||
}
|
||||
|
||||
// each time we get a line of output, we loop!
|
||||
if s := data.text; s == "" {
|
||||
obj.init.Logf("watch output is empty!")
|
||||
} else {
|
||||
obj.init.Logf("watch output is:")
|
||||
obj.init.Logf(s)
|
||||
}
|
||||
if data.text != "" {
|
||||
send = true
|
||||
}
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply checks the resource state and applies the resource if the bool
|
||||
// input is true. It returns error info and if the state check passed or not.
|
||||
// TODO: expand the IfCmd to be a list of commands
|
||||
func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
||||
// If we receive a refresh signal, then the engine skips the IsStateOK()
|
||||
// check and this will run. It is still guarded by the IfCmd, but it can
|
||||
// have a chance to execute, and all without the check of obj.Refresh()!
|
||||
|
||||
if obj.IfCmd != "" { // if there is no onlyif check, we should just run
|
||||
var cmdName string
|
||||
var cmdArgs []string
|
||||
if obj.IfShell == "" {
|
||||
// call without a shell
|
||||
// FIXME: are there still whitespace splitting issues?
|
||||
split := strings.Fields(obj.IfCmd)
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:]
|
||||
} else {
|
||||
cmdName = obj.IfShell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.IfCmd}
|
||||
}
|
||||
cmd := exec.Command(cmdName, cmdArgs...)
|
||||
cmd.Dir = obj.IfCwd // run program in pwd if ""
|
||||
// ignore signals sent to parent process (we're in our own group)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
// if we have an user and group, use them
|
||||
var err error
|
||||
if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error while setting credential")
|
||||
}
|
||||
|
||||
var out splitWriter
|
||||
out.Init()
|
||||
cmd.Stdout = out.Stdout
|
||||
cmd.Stderr = out.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||
if !ok {
|
||||
// command failed in some bad way
|
||||
return false, errwrap.Wrapf(err, "ifcmd failed in some bad way")
|
||||
}
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return false, errwrap.Wrapf(err, "could not get exit status of ifcmd")
|
||||
}
|
||||
exitStatus := wStatus.ExitStatus()
|
||||
if exitStatus == 0 {
|
||||
// i'm not sure if this could happen
|
||||
return false, errwrap.Wrapf(err, "unexpected ifcmd exit status of zero")
|
||||
}
|
||||
|
||||
obj.init.Logf("ifcmd exited with: %d", exitStatus)
|
||||
if s := out.String(); s == "" {
|
||||
obj.init.Logf("ifcmd output is empty!")
|
||||
} else {
|
||||
obj.init.Logf("ifcmd output is:")
|
||||
obj.init.Logf(s)
|
||||
}
|
||||
return true, nil // don't run
|
||||
}
|
||||
if s := out.String(); s == "" {
|
||||
obj.init.Logf("ifcmd output is empty!")
|
||||
} else {
|
||||
obj.init.Logf("ifcmd output is:")
|
||||
obj.init.Logf(s)
|
||||
}
|
||||
}
|
||||
|
||||
// state is not okay, no work done, exit, but without error
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// apply portion
|
||||
obj.init.Logf("Apply")
|
||||
var cmdName string
|
||||
var cmdArgs []string
|
||||
if obj.Shell == "" {
|
||||
// call without a shell
|
||||
// FIXME: are there still whitespace splitting issues?
|
||||
// TODO: we could make the split character user selectable...!
|
||||
split := strings.Fields(obj.getCmd())
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:]
|
||||
if len(obj.Args) > 0 {
|
||||
if len(split) != 1 { // should not happen
|
||||
return false, fmt.Errorf("validation error")
|
||||
}
|
||||
cmdArgs = obj.Args
|
||||
}
|
||||
} else {
|
||||
cmdName = obj.Shell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.getCmd()}
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait() // this must be above the defer cancel() call
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
if obj.Timeout > 0 { // cmd.Process.Kill() is called on timeout
|
||||
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(obj.Timeout)*time.Second)
|
||||
} else { // zero timeout means no timer
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||
cmd.Dir = obj.Cwd // run program in pwd if ""
|
||||
|
||||
envKeys := []string{}
|
||||
for key := range obj.Env {
|
||||
envKeys = append(envKeys, key)
|
||||
}
|
||||
sort.Strings(envKeys)
|
||||
cmdEnv := []string{}
|
||||
for _, k := range envKeys {
|
||||
cmdEnv = append(cmdEnv, k+"="+obj.Env[k])
|
||||
}
|
||||
cmd.Env = cmdEnv
|
||||
|
||||
// ignore signals sent to parent process (we're in our own group)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
// if we have a user and group, use them
|
||||
var err error
|
||||
if cmd.SysProcAttr.Credential, err = obj.getCredential(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error while setting credential")
|
||||
}
|
||||
|
||||
var out splitWriter
|
||||
out.Init()
|
||||
// from the docs: "If Stdout and Stderr are the same writer, at most one
|
||||
// goroutine at a time will call Write." so we trick it here!
|
||||
cmd.Stdout = out.Stdout
|
||||
cmd.Stderr = out.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error starting cmd")
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-obj.interruptChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// let this exit
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait() // we can unblock this with the timeout
|
||||
|
||||
// save in memory for send/recv
|
||||
// we use pointers to strings to indicate if used or not
|
||||
if out.Stdout.Activity || out.Stderr.Activity {
|
||||
str := out.String()
|
||||
obj.output = &str
|
||||
}
|
||||
if out.Stdout.Activity {
|
||||
str := out.Stdout.String()
|
||||
obj.stdout = &str
|
||||
}
|
||||
if out.Stderr.Activity {
|
||||
str := out.Stderr.String()
|
||||
obj.stderr = &str
|
||||
}
|
||||
|
||||
// process the err result from cmd, we process non-zero exits here too!
|
||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||
if err != nil && ok {
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return false, errwrap.Wrapf(err, "error running cmd")
|
||||
}
|
||||
exitStatus := wStatus.ExitStatus()
|
||||
if !wStatus.Signaled() { // not a timeout or cancel (no signal)
|
||||
return false, errwrap.Wrapf(err, "cmd error, exit status: %d", exitStatus)
|
||||
}
|
||||
sig := wStatus.Signal()
|
||||
|
||||
// we get this on timeout, because ctx calls cmd.Process.Kill()
|
||||
if sig == syscall.SIGKILL {
|
||||
return false, errwrap.Wrapf(err, "cmd timeout, exit status: %d", exitStatus)
|
||||
}
|
||||
|
||||
return false, errwrap.Wrapf(err, "unknown cmd error, signal: %s, exit status: %d", sig, exitStatus)
|
||||
|
||||
} else if err != nil {
|
||||
return false, errwrap.Wrapf(err, "general cmd error")
|
||||
}
|
||||
|
||||
// TODO: if we printed the stdout while the command is running, this
|
||||
// would be nice, but it would require terminal log output that doesn't
|
||||
// interleave all the parallel parts which would mix it all up...
|
||||
if s := out.String(); s == "" {
|
||||
obj.init.Logf("command output is empty!")
|
||||
} else {
|
||||
obj.init.Logf("command output is:")
|
||||
obj.init.Logf(s)
|
||||
}
|
||||
|
||||
if err := obj.init.Send(&ExecSends{
|
||||
Output: obj.output,
|
||||
Stdout: obj.stdout,
|
||||
Stderr: obj.stderr,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// The state tracking is for exec resources that can't "detect" their
|
||||
// state, and assume it's invalid when the Watch() function triggers.
|
||||
// If we apply state successfully, we should reset it here so that we
|
||||
// know that we have applied since the state was set not ok by event!
|
||||
// This now happens automatically after the engine runs CheckApply().
|
||||
return false, nil // success
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *ExecRes) Cmp(r engine.Res) error {
|
||||
// we can only compare ExecRes to others of the same resource kind
|
||||
res, ok := r.(*ExecRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Cmd != res.Cmd {
|
||||
return fmt.Errorf("the Cmd differs")
|
||||
}
|
||||
if len(obj.Args) != len(res.Args) {
|
||||
return fmt.Errorf("the Args differ")
|
||||
}
|
||||
for i, a := range obj.Args {
|
||||
if a != res.Args[i] {
|
||||
return fmt.Errorf("the Args differ at index: %d", i)
|
||||
}
|
||||
}
|
||||
if obj.Cwd != res.Cwd {
|
||||
return fmt.Errorf("the Cwd differs")
|
||||
}
|
||||
if obj.Shell != res.Shell {
|
||||
return fmt.Errorf("the Shell differs")
|
||||
}
|
||||
if obj.Timeout != res.Timeout {
|
||||
return fmt.Errorf("the Timeout differs")
|
||||
}
|
||||
|
||||
if obj.WatchCmd != res.WatchCmd {
|
||||
return fmt.Errorf("the WatchCmd differs")
|
||||
}
|
||||
if obj.WatchCwd != res.WatchCwd {
|
||||
return fmt.Errorf("the WatchCwd differs")
|
||||
}
|
||||
if obj.WatchShell != res.WatchShell {
|
||||
return fmt.Errorf("the WatchShell differs")
|
||||
}
|
||||
|
||||
if obj.IfCmd != res.IfCmd {
|
||||
return fmt.Errorf("the IfCmd differs")
|
||||
}
|
||||
if obj.IfCwd != res.IfCwd {
|
||||
return fmt.Errorf("the IfCwd differs")
|
||||
}
|
||||
if obj.IfShell != res.IfShell {
|
||||
return fmt.Errorf("the IfShell differs")
|
||||
}
|
||||
|
||||
if obj.User != res.User {
|
||||
return fmt.Errorf("the User differs")
|
||||
}
|
||||
if obj.Group != res.Group {
|
||||
return fmt.Errorf("the Group differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interrupt is called to ask the execution of this resource to end early.
|
||||
func (obj *ExecRes) Interrupt() error {
|
||||
close(obj.interruptChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecUID is the UID struct for ExecRes.
|
||||
type ExecUID struct {
|
||||
engine.BaseUID
|
||||
Cmd string
|
||||
IfCmd string
|
||||
// TODO: add more elements here
|
||||
}
|
||||
|
||||
// ExecResAutoEdges holds the state of the auto edge generator.
|
||||
type ExecResAutoEdges struct {
|
||||
edges []engine.ResUID
|
||||
pointer int
|
||||
}
|
||||
|
||||
// Next returns the next automatic edge.
|
||||
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
|
||||
if len(obj.edges) == 0 {
|
||||
return nil
|
||||
}
|
||||
value := obj.edges[obj.pointer]
|
||||
obj.pointer++
|
||||
return []engine.ResUID{value}
|
||||
}
|
||||
|
||||
// Test gets results of the earlier Next() call, & returns if we should
|
||||
// continue!
|
||||
func (obj *ExecResAutoEdges) Test(input []bool) bool {
|
||||
if len(obj.edges) <= obj.pointer {
|
||||
return false
|
||||
}
|
||||
if len(input) != 1 { // in case we get given bad data
|
||||
panic(fmt.Sprintf("Expecting a single value!"))
|
||||
}
|
||||
return true // keep going
|
||||
}
|
||||
|
||||
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
|
||||
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
var data []engine.ResUID
|
||||
var reversed = true
|
||||
|
||||
for _, x := range obj.cmdFiles() {
|
||||
data = append(data, &PkgFileUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
path: x, // what matters
|
||||
})
|
||||
data = append(data, &FileUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
path: x,
|
||||
})
|
||||
}
|
||||
if obj.User != "" {
|
||||
data = append(data, &UserUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
name: obj.User,
|
||||
})
|
||||
}
|
||||
if obj.Group != "" {
|
||||
data = append(data, &GroupUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
name: obj.Group,
|
||||
})
|
||||
}
|
||||
|
||||
return &ExecResAutoEdges{
|
||||
edges: data,
|
||||
pointer: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *ExecRes) UIDs() []engine.ResUID {
|
||||
x := &ExecUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
Cmd: obj.getCmd(),
|
||||
IfCmd: obj.IfCmd,
|
||||
// TODO: add more params here
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// ExecSends is the struct of data which is sent after a successful Apply.
|
||||
type ExecSends struct {
|
||||
// Output is the combined stdout and stderr of the command.
|
||||
Output *string `lang:"output"`
|
||||
// Stdout is the stdout of the command.
|
||||
Stdout *string `lang:"stdout"`
|
||||
// Stderr is the stderr of the command.
|
||||
Stderr *string `lang:"stderr"`
|
||||
}
|
||||
|
||||
// Sends represents the default struct of values we can send using Send/Recv.
|
||||
func (obj *ExecRes) Sends() interface{} {
|
||||
return &ExecSends{
|
||||
Output: nil,
|
||||
Stdout: nil,
|
||||
Stderr: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes ExecRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*ExecRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to ExecRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = ExecRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCredential returns the correct *syscall.Credential if an User and Group
|
||||
// are set.
|
||||
func (obj *ExecRes) getCredential() (*syscall.Credential, error) {
|
||||
var uid, gid int
|
||||
var err error
|
||||
var currentUser *user.User
|
||||
if currentUser, err = user.Current(); err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error looking up current user")
|
||||
}
|
||||
if currentUser.Uid != "0" {
|
||||
// since we're not root, we've got nothing to do
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if obj.Group != "" {
|
||||
gid, err = engineUtil.GetGID(obj.Group)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.User != "" {
|
||||
uid, err = engineUtil.GetUID(obj.User)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error looking up uid for %s", obj.User)
|
||||
}
|
||||
}
|
||||
|
||||
return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, nil
|
||||
}
|
||||
|
||||
// cmdFiles returns all the potential files/commands this command might need.
|
||||
func (obj *ExecRes) cmdFiles() []string {
|
||||
var paths []string
|
||||
if obj.Shell != "" {
|
||||
paths = append(paths, obj.Shell)
|
||||
} else if cmdSplit := strings.Fields(obj.getCmd()); len(cmdSplit) > 0 {
|
||||
paths = append(paths, cmdSplit[0])
|
||||
}
|
||||
if obj.WatchShell != "" {
|
||||
paths = append(paths, obj.WatchShell)
|
||||
} else if watchSplit := strings.Fields(obj.WatchCmd); len(watchSplit) > 0 {
|
||||
paths = append(paths, watchSplit[0])
|
||||
}
|
||||
if obj.IfShell != "" {
|
||||
paths = append(paths, obj.IfShell)
|
||||
} else if ifSplit := strings.Fields(obj.IfCmd); len(ifSplit) > 0 {
|
||||
paths = append(paths, ifSplit[0])
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
// cmdOutput is the output struct of the cmdOutputRunner channel output. You
|
||||
// should always check the error first. If it's nil, then you can assume the
|
||||
// text data is good to use.
|
||||
type cmdOutput struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
// cmdOutputRunner wraps the Cmd in with a StdoutPipe scanner and reads for
|
||||
// errors. It runs Start and Wait, and errors runtime things in the channel. If
|
||||
// it can't start up the command, it will fail early. Once it's running, it will
|
||||
// return the channel which can be used for the duration of the process.
|
||||
// Cancelling the context merely unblocks the sending on the output channel, it
|
||||
// does not Kill the cmd process. For that you must do it yourself elsewhere.
|
||||
func (obj *ExecRes) cmdOutputRunner(ctx context.Context, cmd *exec.Cmd) (chan *cmdOutput, error) {
|
||||
cmdReader, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error creating StdoutPipe for Cmd")
|
||||
}
|
||||
scanner := bufio.NewScanner(cmdReader)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error starting Cmd")
|
||||
}
|
||||
|
||||
ch := make(chan *cmdOutput)
|
||||
obj.wg.Add(1)
|
||||
go func() {
|
||||
defer obj.wg.Done()
|
||||
defer close(ch)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case ch <- &cmdOutput{text: scanner.Text()}: // blocks here ?
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// on EOF, scanner.Err() will be nil
|
||||
reterr := scanner.Err()
|
||||
reterr = errwrap.Append(reterr, cmd.Wait()) // always run Wait()
|
||||
// send any misc errors we encounter on the channel
|
||||
if reterr != nil {
|
||||
select {
|
||||
case ch <- &cmdOutput{err: reterr}:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// splitWriter mimics what the ssh.CombinedOutput command does, but stores the
|
||||
// the stdout and stderr separately. This is slightly tricky because we don't
|
||||
// want the combined output to be interleaved incorrectly. It creates sub writer
|
||||
// structs which share the same lock and a shared output buffer.
|
||||
type splitWriter struct {
|
||||
Stdout *wrapWriter
|
||||
Stderr *wrapWriter
|
||||
|
||||
stdout bytes.Buffer // just the stdout
|
||||
stderr bytes.Buffer // just the stderr
|
||||
output bytes.Buffer // combined output
|
||||
mutex *sync.Mutex
|
||||
initialized bool // is this initialized?
|
||||
}
|
||||
|
||||
// Init initializes the splitWriter.
|
||||
func (obj *splitWriter) Init() {
|
||||
if obj.initialized {
|
||||
panic("splitWriter is already initialized")
|
||||
}
|
||||
obj.mutex = &sync.Mutex{}
|
||||
obj.Stdout = &wrapWriter{
|
||||
Mutex: obj.mutex,
|
||||
Buffer: &obj.stdout,
|
||||
Output: &obj.output,
|
||||
}
|
||||
obj.Stderr = &wrapWriter{
|
||||
Mutex: obj.mutex,
|
||||
Buffer: &obj.stderr,
|
||||
Output: &obj.output,
|
||||
}
|
||||
obj.initialized = true
|
||||
}
|
||||
|
||||
// String returns the contents of the combined output buffer.
|
||||
func (obj *splitWriter) String() string {
|
||||
if !obj.initialized {
|
||||
panic("splitWriter is not initialized")
|
||||
}
|
||||
return obj.output.String()
|
||||
}
|
||||
|
||||
// wrapWriter is a simple writer which is used internally by splitWriter.
|
||||
type wrapWriter struct {
|
||||
Mutex *sync.Mutex
|
||||
Buffer *bytes.Buffer // stdout or stderr
|
||||
Output *bytes.Buffer // combined output
|
||||
Activity bool // did we get any writes?
|
||||
}
|
||||
|
||||
// Write writes to both bytes buffers with a parent lock to mix output safely.
|
||||
func (obj *wrapWriter) Write(p []byte) (int, error) {
|
||||
// TODO: can we move the lock to only guard around the Output.Write ?
|
||||
obj.Mutex.Lock()
|
||||
defer obj.Mutex.Unlock()
|
||||
obj.Activity = true
|
||||
i, err := obj.Buffer.Write(p) // first write
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
return obj.Output.Write(p) // shared write
|
||||
}
|
||||
|
||||
// String returns the contents of the unshared buffer.
|
||||
func (obj *wrapWriter) String() string {
|
||||
return obj.Buffer.String()
|
||||
}
|
||||
|
||||
// isNameValid checks that environment variable name is valid.
|
||||
func isNameValid(varName string) error {
|
||||
if varName == "" {
|
||||
return fmt.Errorf("variable name cannot be an empty string")
|
||||
}
|
||||
for i := range varName {
|
||||
c := varName[i]
|
||||
if i == 0 && '0' <= c && c <= '9' {
|
||||
return fmt.Errorf("variable name cannot begin with number")
|
||||
}
|
||||
if !(c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z') {
|
||||
return fmt.Errorf("invalid character in variable name")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
335
engine/resources/exec_test.go
Normal file
335
engine/resources/exec_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/graph/autoedge"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
func fakeExecInit(t *testing.T) (*engine.Init, *ExecSends) {
|
||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||
logf := func(format string, v ...interface{}) {
|
||||
t.Logf("test: "+format, v...)
|
||||
}
|
||||
execSends := &ExecSends{}
|
||||
return &engine.Init{
|
||||
Send: func(st interface{}) error {
|
||||
x, ok := st.(*ExecSends)
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to send")
|
||||
}
|
||||
*execSends = *x // set
|
||||
return nil
|
||||
},
|
||||
Debug: debug,
|
||||
Logf: logf,
|
||||
}, execSends
|
||||
}
|
||||
|
||||
func TestExecSendRecv1(t *testing.T) {
|
||||
r1 := &ExecRes{
|
||||
Cmd: "echo hello world",
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := r1.Close(); err != nil {
|
||||
t.Errorf("close failed with: %v", err)
|
||||
}
|
||||
}()
|
||||
init, execSends := fakeExecInit(t)
|
||||
if err := r1.Init(init); err != nil {
|
||||
t.Errorf("init failed with: %v", err)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
if _, err := r1.CheckApply(true); err != nil {
|
||||
t.Errorf("checkapply failed with: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("output is: %v", execSends.Output)
|
||||
if execSends.Output != nil {
|
||||
t.Logf("output is: %v", *execSends.Output)
|
||||
}
|
||||
t.Logf("stdout is: %v", execSends.Stdout)
|
||||
if execSends.Stdout != nil {
|
||||
t.Logf("stdout is: %v", *execSends.Stdout)
|
||||
}
|
||||
t.Logf("stderr is: %v", execSends.Stderr)
|
||||
if execSends.Stderr != nil {
|
||||
t.Logf("stderr is: %v", *execSends.Stderr)
|
||||
}
|
||||
|
||||
if execSends.Stdout == nil {
|
||||
t.Errorf("stdout is nil")
|
||||
} else {
|
||||
if out := *execSends.Stdout; out != "hello world\n" {
|
||||
t.Errorf("got wrong stdout(%d): %s", len(out), out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecSendRecv2(t *testing.T) {
|
||||
r1 := &ExecRes{
|
||||
Cmd: "echo hello world 1>&2", // to stderr
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := r1.Close(); err != nil {
|
||||
t.Errorf("close failed with: %v", err)
|
||||
}
|
||||
}()
|
||||
init, execSends := fakeExecInit(t)
|
||||
if err := r1.Init(init); err != nil {
|
||||
t.Errorf("init failed with: %v", err)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
if _, err := r1.CheckApply(true); err != nil {
|
||||
t.Errorf("checkapply failed with: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("output is: %v", execSends.Output)
|
||||
if execSends.Output != nil {
|
||||
t.Logf("output is: %v", *execSends.Output)
|
||||
}
|
||||
t.Logf("stdout is: %v", execSends.Stdout)
|
||||
if execSends.Stdout != nil {
|
||||
t.Logf("stdout is: %v", *execSends.Stdout)
|
||||
}
|
||||
t.Logf("stderr is: %v", execSends.Stderr)
|
||||
if execSends.Stderr != nil {
|
||||
t.Logf("stderr is: %v", *execSends.Stderr)
|
||||
}
|
||||
|
||||
if execSends.Stderr == nil {
|
||||
t.Errorf("stderr is nil")
|
||||
} else {
|
||||
if out := *execSends.Stderr; out != "hello world\n" {
|
||||
t.Errorf("got wrong stderr(%d): %s", len(out), out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecSendRecv3(t *testing.T) {
|
||||
r1 := &ExecRes{
|
||||
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := r1.Close(); err != nil {
|
||||
t.Errorf("close failed with: %v", err)
|
||||
}
|
||||
}()
|
||||
init, execSends := fakeExecInit(t)
|
||||
if err := r1.Init(init); err != nil {
|
||||
t.Errorf("init failed with: %v", err)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
if _, err := r1.CheckApply(true); err != nil {
|
||||
t.Errorf("checkapply failed with: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("output is: %v", execSends.Output)
|
||||
if execSends.Output != nil {
|
||||
t.Logf("output is: %v", *execSends.Output)
|
||||
}
|
||||
t.Logf("stdout is: %v", execSends.Stdout)
|
||||
if execSends.Stdout != nil {
|
||||
t.Logf("stdout is: %v", *execSends.Stdout)
|
||||
}
|
||||
t.Logf("stderr is: %v", execSends.Stderr)
|
||||
if execSends.Stderr != nil {
|
||||
t.Logf("stderr is: %v", *execSends.Stderr)
|
||||
}
|
||||
|
||||
if execSends.Output == nil {
|
||||
t.Errorf("output is nil")
|
||||
} else {
|
||||
// it looks like bash or golang race to the write, so whichever
|
||||
// order they come out in is ok, as long as they come out whole
|
||||
if out := *execSends.Output; out != "hello world\ngoodbye world\n" && out != "goodbye world\nhello world\n" {
|
||||
t.Errorf("got wrong output(%d): %s", len(out), out)
|
||||
}
|
||||
}
|
||||
|
||||
if execSends.Stdout == nil {
|
||||
t.Errorf("stdout is nil")
|
||||
} else {
|
||||
if out := *execSends.Stdout; out != "hello world\n" {
|
||||
t.Errorf("got wrong stdout(%d): %s", len(out), out)
|
||||
}
|
||||
}
|
||||
|
||||
if execSends.Stderr == nil {
|
||||
t.Errorf("stderr is nil")
|
||||
} else {
|
||||
if out := *execSends.Stderr; out != "goodbye world\n" {
|
||||
t.Errorf("got wrong stderr(%d): %s", len(out), out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecTimeoutBehaviour(t *testing.T) {
|
||||
// cmd.Process.Kill() is called on timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
cmdName := "/bin/sleep" // it's /usr/bin/sleep on modern distros
|
||||
cmdArgs := []string{"300"} // 5 min in seconds
|
||||
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||
// ignore signals sent to parent process (we're in our own group)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Errorf("error starting cmd: %+v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err := cmd.Wait() // we can unblock this with the timeout
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
return
|
||||
}
|
||||
|
||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||
if err != nil && ok {
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
t.Errorf("error running cmd")
|
||||
return
|
||||
}
|
||||
if !wStatus.Signaled() {
|
||||
t.Errorf("did not get signal, exit status: %d", wStatus.ExitStatus())
|
||||
return
|
||||
}
|
||||
|
||||
// we get this on timeout, because ctx calls cmd.Process.Kill()
|
||||
if sig := wStatus.Signal(); sig != syscall.SIGKILL {
|
||||
t.Errorf("got wrong signal: %+v, exit status: %d", sig, wStatus.ExitStatus())
|
||||
return
|
||||
}
|
||||
|
||||
t.Logf("exit status: %d", wStatus.ExitStatus())
|
||||
return
|
||||
|
||||
} else if err != nil {
|
||||
t.Errorf("general cmd error")
|
||||
return
|
||||
}
|
||||
|
||||
// no error
|
||||
}
|
||||
|
||||
func TestExecAutoEdge1(t *testing.T) {
|
||||
g, err := pgraph.NewGraph("TestGraph")
|
||||
if err != nil {
|
||||
t.Errorf("error creating graph: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resUser, err := engine.NewNamedResource("user", "someuser")
|
||||
if err != nil {
|
||||
t.Errorf("error creating user resource: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resGroup, err := engine.NewNamedResource("group", "somegroup")
|
||||
if err != nil {
|
||||
t.Errorf("error creating group resource: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resFile, err := engine.NewNamedResource("file", "/somefile")
|
||||
if err != nil {
|
||||
t.Errorf("error creating group resource: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
resExec, err := engine.NewNamedResource("exec", "somefile")
|
||||
if err != nil {
|
||||
t.Errorf("error creating exec resource: %v", err)
|
||||
return
|
||||
}
|
||||
exc := resExec.(*ExecRes)
|
||||
exc.Cmd = resFile.Name()
|
||||
exc.User = resUser.Name()
|
||||
exc.Group = resGroup.Name()
|
||||
|
||||
g.AddVertex(resUser, resGroup, resFile, resExec)
|
||||
|
||||
if i := g.NumEdges(); i != 0 {
|
||||
t.Errorf("should have 0 edges instead of: %d", i)
|
||||
return
|
||||
}
|
||||
|
||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||
logf := func(format string, v ...interface{}) {
|
||||
t.Logf("test: "+format, v...)
|
||||
}
|
||||
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
|
||||
t.Errorf("error running autoedges: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expected, err := pgraph.NewGraph("Expected")
|
||||
if err != nil {
|
||||
t.Errorf("error creating graph: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
expectEdge := func(from, to pgraph.Vertex) {
|
||||
edge := &engine.Edge{Name: fmt.Sprintf("%s -> %s (expected)", from, to)}
|
||||
expected.AddEdge(from, to, edge)
|
||||
}
|
||||
expectEdge(resFile, resExec)
|
||||
expectEdge(resUser, resExec)
|
||||
expectEdge(resGroup, resExec)
|
||||
|
||||
vertexCmp := func(v1, v2 pgraph.Vertex) (bool, error) { return v1 == v2, nil } // pointer compare is sufficient
|
||||
edgeCmp := func(e1, e2 pgraph.Edge) (bool, error) { return true, nil } // we don't care about edges here
|
||||
|
||||
if err := expected.GraphCmp(g, vertexCmp, edgeCmp); err != nil {
|
||||
t.Errorf("graph doesn't match expected: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
1680
engine/resources/file.go
Normal file
1680
engine/resources/file.go
Normal file
File diff suppressed because it is too large
Load Diff
265
engine/resources/file_test.go
Normal file
265
engine/resources/file_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/graph/autoedge"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
)
|
||||
|
||||
func TestFileAutoEdge1(t *testing.T) {
|
||||
|
||||
g, err := pgraph.NewGraph("TestGraph")
|
||||
if err != nil {
|
||||
t.Errorf("error creating graph: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
r1 := &FileRes{
|
||||
Path: "/tmp/a/b/", // some dir
|
||||
}
|
||||
r2 := &FileRes{
|
||||
Path: "/tmp/a/", // some parent dir
|
||||
}
|
||||
r3 := &FileRes{
|
||||
Path: "/tmp/a/b/c", // some child file
|
||||
}
|
||||
g.AddVertex(r1, r2, r3)
|
||||
|
||||
if i := g.NumEdges(); i != 0 {
|
||||
t.Errorf("should have 0 edges instead of: %d", i)
|
||||
}
|
||||
|
||||
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||
logf := func(format string, v ...interface{}) {
|
||||
t.Logf("test: "+format, v...)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
|
||||
t.Errorf("error running autoedges: %v", err)
|
||||
}
|
||||
|
||||
// two edges should have been added
|
||||
if i := g.NumEdges(); i != 2 {
|
||||
t.Errorf("should have 2 edges instead of: %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscEncodeDecode1(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// encode
|
||||
var input interface{} = &FileRes{}
|
||||
b1 := bytes.Buffer{}
|
||||
e := gob.NewEncoder(&b1)
|
||||
err = e.Encode(&input) // pass with &
|
||||
if err != nil {
|
||||
t.Errorf("gob failed to Encode: %v", err)
|
||||
}
|
||||
str := base64.StdEncoding.EncodeToString(b1.Bytes())
|
||||
|
||||
// decode
|
||||
var output interface{}
|
||||
bb, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
t.Errorf("base64 failed to Decode: %v", err)
|
||||
}
|
||||
b2 := bytes.NewBuffer(bb)
|
||||
d := gob.NewDecoder(b2)
|
||||
err = d.Decode(&output) // pass with &
|
||||
if err != nil {
|
||||
t.Errorf("gob failed to Decode: %v", err)
|
||||
}
|
||||
|
||||
res1, ok := input.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("input %v is not a Res", res1)
|
||||
return
|
||||
}
|
||||
res2, ok := output.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("output %v is not a Res", res2)
|
||||
return
|
||||
}
|
||||
if err := res1.Cmp(res2); err != nil {
|
||||
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscEncodeDecode2(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// encode
|
||||
input, err := engine.NewNamedResource("file", "file1")
|
||||
if err != nil {
|
||||
t.Errorf("can't create: %v", err)
|
||||
return
|
||||
}
|
||||
// NOTE: Do not add this bit of code, because it would cause the path to
|
||||
// get taken from the actual Path parameter, instead of using the name,
|
||||
// and if we use the name, the Cmp function will detect if the name is
|
||||
// stored properly or not.
|
||||
//fileRes := input.(*FileRes) // must not panic
|
||||
//fileRes.Path = "/tmp/whatever"
|
||||
|
||||
b64, err := engineUtil.ResToB64(input)
|
||||
if err != nil {
|
||||
t.Errorf("can't encode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := engineUtil.B64ToRes(b64)
|
||||
if err != nil {
|
||||
t.Errorf("can't decode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res1, ok := input.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("input %v is not a Res", res1)
|
||||
return
|
||||
}
|
||||
res2, ok := output.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("output %v is not a Res", res2)
|
||||
return
|
||||
}
|
||||
// this uses the standalone file cmp function
|
||||
if err := res1.Cmp(res2); err != nil {
|
||||
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscEncodeDecode3(t *testing.T) {
|
||||
var err error
|
||||
|
||||
// encode
|
||||
input, err := engine.NewNamedResource("file", "file1")
|
||||
if err != nil {
|
||||
t.Errorf("can't create: %v", err)
|
||||
return
|
||||
}
|
||||
fileRes := input.(*FileRes) // must not panic
|
||||
fileRes.Path = "/tmp/whatever"
|
||||
// TODO: add other params/traits/etc here!
|
||||
|
||||
b64, err := engineUtil.ResToB64(input)
|
||||
if err != nil {
|
||||
t.Errorf("can't encode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := engineUtil.B64ToRes(b64)
|
||||
if err != nil {
|
||||
t.Errorf("can't decode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res1, ok := input.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("input %v is not a Res", res1)
|
||||
return
|
||||
}
|
||||
res2, ok := output.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("output %v is not a Res", res2)
|
||||
return
|
||||
}
|
||||
// this uses the more complete, engine cmp function
|
||||
if err := engine.ResCmp(res1, res2); err != nil {
|
||||
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscEncodeDecode4(t *testing.T) {
|
||||
var err error
|
||||
const (
|
||||
Kind = "file"
|
||||
Name = "file1"
|
||||
)
|
||||
|
||||
// encode
|
||||
input, err := engine.NewNamedResource(Kind, Name)
|
||||
if err != nil {
|
||||
t.Errorf("can't create: %v", err)
|
||||
return
|
||||
}
|
||||
fileRes := input.(*FileRes) // must not panic
|
||||
fileRes.Path = "/tmp/whatever"
|
||||
// TODO: add other params/traits/etc here!
|
||||
|
||||
b64, err := engineUtil.ResToB64(input)
|
||||
if err != nil {
|
||||
t.Errorf("can't encode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := engineUtil.B64ToRes(b64)
|
||||
if err != nil {
|
||||
t.Errorf("can't decode: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
res1, ok := input.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("input %v is not a Res", res1)
|
||||
return
|
||||
}
|
||||
res2, ok := output.(engine.Res)
|
||||
if !ok {
|
||||
t.Errorf("output %v is not a Res", res2)
|
||||
return
|
||||
}
|
||||
// this uses the more complete, engine cmp function
|
||||
if err := engine.ResCmp(res1, res2); err != nil {
|
||||
t.Errorf("the input and output Res values do not match: %+v", err)
|
||||
}
|
||||
|
||||
// ensure the kind and name are correctly decoded too!
|
||||
if kind := res2.Kind(); kind != Kind {
|
||||
t.Errorf("the output kind was `%s`, expected `%s`", kind, Kind)
|
||||
}
|
||||
if name := res2.Name(); name != Name {
|
||||
t.Errorf("the output name was `%s`, expected `%s`", name, Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileAbsolute1(t *testing.T) {
|
||||
// file resource paths should be absolute
|
||||
f1 := &FileRes{
|
||||
Path: "tmp/a/b", // some relative file
|
||||
}
|
||||
f2 := &FileRes{
|
||||
Path: "tmp/a/b/", // some relative dir
|
||||
}
|
||||
f3 := &FileRes{
|
||||
Path: "tmp", // some short relative file
|
||||
}
|
||||
if f1.Validate() == nil || f2.Validate() == nil || f3.Validate() == nil {
|
||||
t.Errorf("file res should have failed validate")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,52 +20,59 @@ package resources
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("group", func() Res { return &GroupRes{} })
|
||||
engine.RegisterResource("group", func() engine.Res { return &GroupRes{} })
|
||||
}
|
||||
|
||||
const groupFile = "/etc/group"
|
||||
|
||||
// GroupRes is a user group resource.
|
||||
type GroupRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"` // state: exists, absent
|
||||
GID *uint32 `yaml:"gid"` // the group's gid
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
State string `yaml:"state"` // state: exists, absent
|
||||
GID *uint32 `yaml:"gid"` // the group's gid
|
||||
|
||||
recWatcher *recwatch.RecWatcher
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *GroupRes) Default() Res {
|
||||
return &GroupRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *GroupRes) Default() engine.Res {
|
||||
return &GroupRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *GroupRes) Validate() error {
|
||||
if obj.State != "exists" && obj.State != "absent" {
|
||||
return fmt.Errorf("State must be 'exists' or 'absent'")
|
||||
return fmt.Errorf("state must be 'exists' or 'absent'")
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the resource.
|
||||
func (obj *GroupRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *GroupRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *GroupRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
@@ -77,17 +84,12 @@ func (obj *GroupRes) Watch() error {
|
||||
}
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching: %s", obj, groupFile) // attempting to watch...
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", groupFile) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -98,34 +100,30 @@ func (obj *GroupRes) Watch() error {
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.debug { // don't access event.Body if event.Error isn't nil
|
||||
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op)
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
//obj.StateOK(false) // dirty // these events don't invalidate state
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Group resource.
|
||||
func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply(%t)", obj, apply)
|
||||
func (obj *GroupRes) CheckApply(apply bool) (bool, error) {
|
||||
obj.init.Logf("CheckApply(%t)", apply)
|
||||
|
||||
// check if the group exists
|
||||
exists := true
|
||||
group, err := user.LookupGroup(obj.GetName())
|
||||
group, err := user.LookupGroup(obj.Name())
|
||||
if err != nil {
|
||||
if _, ok := err.(user.UnknownGroupError); !ok {
|
||||
return false, errwrap.Wrapf(err, "error looking up group")
|
||||
@@ -148,7 +146,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, errwrap.Wrapf(err, "error looking up GID")
|
||||
}
|
||||
}
|
||||
if lookupGID != nil && lookupGID.Name != obj.GetName() {
|
||||
if lookupGID != nil && lookupGID.Name != obj.Name() {
|
||||
return false, fmt.Errorf("the requested GID belongs to another group")
|
||||
}
|
||||
// get the existing group's GID
|
||||
@@ -159,7 +157,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
// check if existing group has the wrong GID
|
||||
// if it is wrong groupmod will change it to the desired value
|
||||
if *obj.GID != uint32(existingGID) {
|
||||
log.Printf("%s: Inconsistent GID: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Inconsistent GID: %s", obj.Name())
|
||||
}
|
||||
// if the group exists and has the correct GID, we are done
|
||||
if obj.State == "exists" && *obj.GID == uint32(existingGID) {
|
||||
@@ -172,14 +170,14 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
var cmdName string
|
||||
args := []string{obj.GetName()}
|
||||
args := []string{obj.Name()}
|
||||
|
||||
if obj.State == "exists" {
|
||||
if exists {
|
||||
log.Printf("%s: Modifying group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Modifying group: %s", obj.Name())
|
||||
cmdName = "groupmod"
|
||||
} else {
|
||||
log.Printf("%s: Adding group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Adding group: %s", obj.Name())
|
||||
cmdName = "groupadd"
|
||||
}
|
||||
if obj.GID != nil {
|
||||
@@ -187,7 +185,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
}
|
||||
if obj.State == "absent" && exists {
|
||||
log.Printf("%s: Deleting group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Deleting group: %s", obj.Name())
|
||||
cmdName = "groupdel"
|
||||
}
|
||||
|
||||
@@ -220,15 +218,42 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *GroupRes) Cmp(r engine.Res) error {
|
||||
// we can only compare GroupRes to others of the same resource kind
|
||||
res, ok := r.(*GroupRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
if (obj.GID == nil) != (res.GID == nil) {
|
||||
return fmt.Errorf("the GID differs")
|
||||
}
|
||||
if obj.GID != nil && res.GID != nil {
|
||||
if *obj.GID != *res.GID {
|
||||
return fmt.Errorf("the GID differs")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupUID is the UID struct for GroupRes.
|
||||
type GroupUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string
|
||||
gid *uint32
|
||||
}
|
||||
|
||||
// AutoEdges returns the AutoEdge interface.
|
||||
func (obj *GroupRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *GroupUID) IFF(uid ResUID) bool {
|
||||
func (obj *GroupUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*GroupUID)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -246,55 +271,19 @@ func (obj *GroupUID) IFF(uid ResUID) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *GroupRes) UIDs() []ResUID {
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *GroupRes) UIDs() []engine.ResUID {
|
||||
x := &GroupUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
gid: obj.GID,
|
||||
}
|
||||
return []ResUID{x}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *GroupRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*GroupRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *GroupRes) Compare(r Res) bool {
|
||||
// we can only compare GroupRes to others of the same resource kind
|
||||
res, ok := r.(*GroupRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
}
|
||||
if (obj.GID == nil) != (res.GID == nil) {
|
||||
return false
|
||||
}
|
||||
if obj.GID != nil && res.GID != nil {
|
||||
if *obj.GID != *res.GID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *GroupRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes GroupRes // indirection to avoid infinite recursion
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,37 +20,38 @@ package resources
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"github.com/godbus/dbus"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrResourceInsufficientParameters is returned when the configuration of the resource
|
||||
// is insufficient for the resource to do any useful work.
|
||||
var ErrResourceInsufficientParameters = errors.New(
|
||||
"Insufficient parameters for this resource")
|
||||
|
||||
func init() {
|
||||
RegisterResource("hostname", func() Res { return &HostnameRes{} })
|
||||
engine.RegisterResource("hostname", func() engine.Res { return &HostnameRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
hostname1Path = "/org/freedesktop/hostname1"
|
||||
hostname1Iface = "org.freedesktop.hostname1"
|
||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
||||
hostname1Path = "/org/freedesktop/hostname1"
|
||||
hostname1Iface = "org.freedesktop.hostname1"
|
||||
dbusPropertiesIface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
// ErrResourceInsufficientParameters is returned when the configuration of the
|
||||
// resource is insufficient for the resource to do any useful work.
|
||||
var ErrResourceInsufficientParameters = errors.New("insufficient parameters for this resource")
|
||||
|
||||
// HostnameRes is a resource that allows setting and watching the hostname.
|
||||
//
|
||||
// StaticHostname is the one configured in /etc/hostname or a similar file.
|
||||
// It is chosen by the local user. It is not always in sync with the current
|
||||
// host name as returned by the gethostname() system call.
|
||||
// StaticHostname is the one configured in /etc/hostname or a similar file. It
|
||||
// is chosen by the local user. It is not always in sync with the current host
|
||||
// name as returned by the gethostname() system call.
|
||||
//
|
||||
// TransientHostname is the one configured via the kernel's sethostbyname().
|
||||
// It can be different from the static hostname in case DHCP or mDNS have been
|
||||
// TransientHostname is the one configured via the kernel's sethostbyname(). It
|
||||
// can be different from the static hostname in case DHCP or mDNS have been
|
||||
// configured to change the name based on network information.
|
||||
//
|
||||
// PrettyHostname is a free-form UTF8 host name for presentation to the user.
|
||||
@@ -58,7 +59,10 @@ const (
|
||||
// Hostname is the fallback value for all 3 fields above, if only Hostname is
|
||||
// specified, it will set all 3 fields to this value.
|
||||
type HostnameRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Hostname string `yaml:"hostname"`
|
||||
PrettyHostname string `yaml:"pretty_hostname"`
|
||||
StaticHostname string `yaml:"static_hostname"`
|
||||
@@ -68,12 +72,8 @@ type HostnameRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HostnameRes) Default() Res {
|
||||
return &HostnameRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *HostnameRes) Default() engine.Res {
|
||||
return &HostnameRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
@@ -81,11 +81,13 @@ func (obj *HostnameRes) Validate() error {
|
||||
if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" {
|
||||
return ErrResourceInsufficientParameters
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *HostnameRes) Init() error {
|
||||
func (obj *HostnameRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
if obj.PrettyHostname == "" {
|
||||
obj.PrettyHostname = obj.Hostname
|
||||
}
|
||||
@@ -95,7 +97,12 @@ func (obj *HostnameRes) Init() error {
|
||||
if obj.TransientHostname == "" {
|
||||
obj.TransientHostname = obj.Hostname
|
||||
}
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *HostnameRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
@@ -103,61 +110,55 @@ func (obj *HostnameRes) Watch() error {
|
||||
// if we share the bus with others, we will get each others messages!!
|
||||
bus, err := util.SystemBusPrivateUsable() // don't share the bus connection!
|
||||
if err != nil {
|
||||
return errwrap.Wrap(err, "Failed to connect to bus")
|
||||
return errwrap.Wrapf(err, "failed to connect to bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
callResult := bus.BusObject().Call(
|
||||
"org.freedesktop.DBus.AddMatch", 0,
|
||||
fmt.Sprintf("type='signal',path='%s',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", hostname1Path))
|
||||
if callResult.Err != nil {
|
||||
return errwrap.Wrap(callResult.Err, "Failed to subscribe to DBus events for hostname1")
|
||||
// watch the PropertiesChanged signal on the hostname1 dbus path
|
||||
args := fmt.Sprintf(
|
||||
"type='signal', path='%s', interface='%s', member='PropertiesChanged'",
|
||||
hostname1Path,
|
||||
dbusPropertiesIface,
|
||||
)
|
||||
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
return errwrap.Wrapf(call.Err, "failed to subscribe to DBus events for hostname1")
|
||||
}
|
||||
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||
|
||||
signals := make(chan *dbus.Signal, 10) // closed by dbus package
|
||||
bus.Signal(signals)
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-signals:
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, _ := obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) {
|
||||
func (obj *HostnameRes) updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (bool, error) {
|
||||
propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property)
|
||||
}
|
||||
if propertyObject.Value() == nil {
|
||||
return false, errwrap.Errorf("Unexpected nil value received when reading property %s", property)
|
||||
return false, fmt.Errorf("unexpected nil value received when reading property %s", property)
|
||||
}
|
||||
|
||||
propertyValue, ok := propertyObject.Value().(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("Received unexpected type as %s value, expected string got '%T'", property, propertyValue)
|
||||
return false, fmt.Errorf("received unexpected type as %s value, expected string got '%T'", property, propertyValue)
|
||||
}
|
||||
|
||||
// expected value and actual value match => checkOk
|
||||
@@ -171,7 +172,7 @@ func updateHostnameProperty(object dbus.BusObject, expectedValue, property, sett
|
||||
}
|
||||
|
||||
// attempting to apply the changes
|
||||
log.Printf("Changing %s: %s => %s", property, propertyValue, expectedValue)
|
||||
obj.init.Logf("Changing %s: %s => %s", property, propertyValue, expectedValue)
|
||||
if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName)
|
||||
}
|
||||
@@ -181,32 +182,32 @@ func updateHostnameProperty(object dbus.BusObject, expectedValue, property, sett
|
||||
}
|
||||
|
||||
// CheckApply method for Hostname resource.
|
||||
func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
func (obj *HostnameRes) CheckApply(apply bool) (bool, error) {
|
||||
conn, err := util.SystemBusPrivateUsable()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrap(err, "Failed to connect to the private system bus")
|
||||
return false, errwrap.Wrapf(err, "failed to connect to the private system bus")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
hostnameObject := conn.Object(hostname1Iface, hostname1Path)
|
||||
|
||||
checkOK = true
|
||||
checkOK := true
|
||||
if obj.PrettyHostname != "" {
|
||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
|
||||
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
checkOK = checkOK && propertyCheckOK
|
||||
}
|
||||
if obj.StaticHostname != "" {
|
||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
|
||||
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
checkOK = checkOK && propertyCheckOK
|
||||
}
|
||||
if obj.TransientHostname != "" {
|
||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
|
||||
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -216,62 +217,52 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HostnameRes) Cmp(r engine.Res) error {
|
||||
// we can only compare HostnameRes to others of the same resource kind
|
||||
res, ok := r.(*HostnameRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.PrettyHostname != res.PrettyHostname {
|
||||
return fmt.Errorf("the PrettyHostname differs")
|
||||
}
|
||||
if obj.StaticHostname != res.StaticHostname {
|
||||
return fmt.Errorf("the StaticHostname differs")
|
||||
}
|
||||
if obj.TransientHostname != res.TransientHostname {
|
||||
return fmt.Errorf("the TransientHostname differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HostnameUID is the UID struct for HostnameRes.
|
||||
type HostnameUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
|
||||
name string
|
||||
prettyHostname string
|
||||
staticHostname string
|
||||
transientHostname string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *HostnameRes) UIDs() []ResUID {
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *HostnameRes) UIDs() []engine.ResUID {
|
||||
x := &HostnameUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
prettyHostname: obj.PrettyHostname,
|
||||
staticHostname: obj.StaticHostname,
|
||||
transientHostname: obj.TransientHostname,
|
||||
}
|
||||
return []ResUID{x}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *HostnameRes) GroupCmp(r Res) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *HostnameRes) Compare(r Res) bool {
|
||||
// we can only compare HostnameRes to others of the same resource kind
|
||||
res, ok := r.(*HostnameRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.PrettyHostname != res.PrettyHostname {
|
||||
return false
|
||||
}
|
||||
if obj.StaticHostname != res.StaticHostname {
|
||||
return false
|
||||
}
|
||||
if obj.TransientHostname != res.TransientHostname {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes HostnameRes // indirection to avoid infinite recursion
|
||||
|
||||
808
engine/resources/http.go
Normal file
808
engine/resources/http.go
Normal file
@@ -0,0 +1,808 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
securefilepath "github.com/cyphar/filepath-securejoin"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("http:server", func() engine.Res { return &HTTPServerRes{} })
|
||||
engine.RegisterResource("http:file", func() engine.Res { return &HTTPFileRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
// HTTPUseSecureJoin specifies that we should add in a "secure join" lib
|
||||
// so that we avoid the ../../etc/passwd and symlink problems.
|
||||
HTTPUseSecureJoin = true
|
||||
)
|
||||
|
||||
// HTTPServerRes is an http server resource. It serves files, but does not
|
||||
// actually apply any state. The name is used as the address to listen on,
|
||||
// unless the Address field is specified, and in that case it is used instead.
|
||||
// This resource can offer up files for serving that are specified either inline
|
||||
// in this resource by specifying an http root, or as http:file resources which
|
||||
// will get autogrouped into this resource at runtime. The two methods can be
|
||||
// combined as well.
|
||||
//
|
||||
// This server also supports autogrouping some more magical resources into it.
|
||||
// For example, the http:flag and http:ui resources add in magic endpoints.
|
||||
//
|
||||
// This server is not meant as a featureful replacement for the venerable and
|
||||
// modern httpd servers out there, but rather as a simple, dynamic, integrated
|
||||
// alternative for bootstrapping new machines and clusters in an elegant way.
|
||||
//
|
||||
// TODO: add support for TLS
|
||||
// XXX: Add an http:flag resource that lets an http client set a flag somewhere!
|
||||
// XXX: Add a http:ui resource that functions can read data from!
|
||||
// XXX: The http:ui resource can also take in values from those functions!
|
||||
type HTTPServerRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable // XXX: add autoedge support
|
||||
traits.Groupable // can have HTTPFileRes grouped into it
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Address is the listen address to use for the http server. It is
|
||||
// common to use `:80` (the standard) to listen on TCP port 80 on all
|
||||
// addresses.
|
||||
Address string `lang:"address" yaml:"address"`
|
||||
|
||||
// Timeout is the maximum duration in seconds to use for unspecified
|
||||
// timeouts. In other words, when this value is specified, it is used as
|
||||
// the value for the other *Timeout values when they aren't used. Put
|
||||
// another way, this makes it easy to set all the different timeouts
|
||||
// with a single parameter.
|
||||
Timeout *uint64 `lang:"timeout" yaml:"timeout"`
|
||||
|
||||
// ReadTimeout is the maximum duration in seconds for reading during the
|
||||
// http request. If it is zero, then there is no timeout. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
// For more information, see the golang net/http Server documentation.
|
||||
ReadTimeout *uint64 `lang:"read_timeout" yaml:"read_timeout"`
|
||||
|
||||
// WriteTimeout is the maximum duration in seconds for writing during
|
||||
// the http request. If it is zero, then there is no timeout. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
// For more information, see the golang net/http Server documentation.
|
||||
WriteTimeout *uint64 `lang:"write_timeout" yaml:"write_timeout"`
|
||||
|
||||
// ShutdownTimeout is the maximum duration in seconds to wait for the
|
||||
// server to shutdown gracefully before calling Close. By default it is
|
||||
// nice to let client connections terminate gracefully, however it might
|
||||
// take longer than we are willing to wait, particularly if one is long
|
||||
// polling or running a very long download. As a result, you can set a
|
||||
// timeout here. The default is zero which means it will wait
|
||||
// indefinitely. The shutdown process can also be cancelled by the
|
||||
// interrupt handler which this resource supports. If this is
|
||||
// unspecified, then the value of Timeout is used instead if it is set.
|
||||
ShutdownTimeout *uint64 `lang:"shutdown_timeout" yaml:"shutdown_timeout"`
|
||||
|
||||
// Root is the root directory that we should serve files from. If it is
|
||||
// not specified, then it is not used. Any http file resources will have
|
||||
// precedence over anything in here, in case the same path exists twice.
|
||||
// TODO: should we have a flag to determine the precedence rules here?
|
||||
Root string `lang:"root" yaml:"root"`
|
||||
|
||||
// TODO: should we allow adding a list of one-of files directly here?
|
||||
|
||||
interruptChan chan struct{}
|
||||
|
||||
conn net.Listener
|
||||
serveMux *http.ServeMux // can't share the global one between resources!
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HTTPServerRes) Default() engine.Res {
|
||||
return &HTTPServerRes{}
|
||||
}
|
||||
|
||||
// getAddress returns the actual address to use. When Address is not specified,
|
||||
// we use the Name.
|
||||
func (obj *HTTPServerRes) getAddress() string {
|
||||
if obj.Address != "" {
|
||||
return obj.Address
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// getReadTimeout determines the value for ReadTimeout, because if unspecified,
|
||||
// this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getReadTimeout() *uint64 {
|
||||
if obj.ReadTimeout != nil {
|
||||
return obj.ReadTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// getWriteTimeout determines the value for WriteTimeout, because if
|
||||
// unspecified, this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getWriteTimeout() *uint64 {
|
||||
if obj.WriteTimeout != nil {
|
||||
return obj.WriteTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// getShutdownTimeout determines the value for ShutdownTimeout, because if
|
||||
// unspecified, this will default to the value of Timeout.
|
||||
func (obj *HTTPServerRes) getShutdownTimeout() *uint64 {
|
||||
if obj.ShutdownTimeout != nil {
|
||||
return obj.ShutdownTimeout
|
||||
}
|
||||
return obj.Timeout // might be nil
|
||||
}
|
||||
|
||||
// Validate checks if the resource data structure was populated correctly.
|
||||
func (obj *HTTPServerRes) Validate() error {
|
||||
if obj.getAddress() == "" {
|
||||
return fmt.Errorf("empty address")
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(obj.getAddress())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the Address is in an invalid format: %s", obj.getAddress())
|
||||
}
|
||||
if host != "" {
|
||||
// TODO: should we allow fqdn's here?
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("the Address is not a valid IP: %s", host)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Root != "" && !strings.HasPrefix(obj.Root, "/") {
|
||||
return fmt.Errorf("the Root must be absolute")
|
||||
}
|
||||
if obj.Root != "" && !strings.HasSuffix(obj.Root, "/") {
|
||||
return fmt.Errorf("the Root must be a dir")
|
||||
}
|
||||
|
||||
// XXX: validate that the autogrouped resources don't have paths that
|
||||
// conflict with each other. We can only have a single unique entry for
|
||||
// what handles a /whatever URL.
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *HTTPServerRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
// No need to error in Validate if Timeout is ignored, but log it.
|
||||
// These are all specified, so Timeout effectively does nothing.
|
||||
a := obj.ReadTimeout != nil
|
||||
b := obj.WriteTimeout != nil
|
||||
c := obj.ShutdownTimeout != nil
|
||||
if obj.Timeout != nil && (a && b && c) {
|
||||
obj.init.Logf("the Timeout param is being ignored")
|
||||
}
|
||||
|
||||
// NOTE: If we don't Init anything that's autogrouped, then it won't
|
||||
// even get an Init call on it.
|
||||
// TODO: should we do this in the engine? Do we want to decide it here?
|
||||
for _, res := range obj.GetGroup() { // grouped elements
|
||||
if err := res.Init(init); err != nil {
|
||||
return errwrap.Wrapf(err, "autogrouped Init failed")
|
||||
}
|
||||
}
|
||||
|
||||
obj.interruptChan = make(chan struct{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *HTTPServerRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *HTTPServerRes) Watch() error {
|
||||
// TODO: I think we could replace all this with:
|
||||
//obj.conn, err := net.Listen("tcp", obj.getAddress())
|
||||
// ...but what is the advantage?
|
||||
addr, err := net.ResolveTCPAddr("tcp", obj.getAddress())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not resolve address")
|
||||
}
|
||||
|
||||
obj.conn, err = net.ListenTCP("tcp", addr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not start listener")
|
||||
}
|
||||
defer obj.conn.Close()
|
||||
|
||||
obj.serveMux = http.NewServeMux() // do it here in case Watch restarts!
|
||||
obj.serveMux.HandleFunc("/", obj.handler())
|
||||
|
||||
readTimeout := uint64(0)
|
||||
if i := obj.getReadTimeout(); i != nil {
|
||||
readTimeout = *i
|
||||
}
|
||||
writeTimeout := uint64(0)
|
||||
if i := obj.getWriteTimeout(); i != nil {
|
||||
writeTimeout = *i
|
||||
}
|
||||
obj.server = &http.Server{
|
||||
Addr: obj.getAddress(),
|
||||
Handler: obj.serveMux,
|
||||
ReadTimeout: time.Duration(readTimeout) * time.Second,
|
||||
WriteTimeout: time.Duration(writeTimeout) * time.Second,
|
||||
//MaxHeaderBytes: 1 << 20, XXX: should we add a param for this?
|
||||
}
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var closeError error
|
||||
closeSignal := make(chan struct{})
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
shutdownChan := make(chan struct{}) // server shutdown finished signal
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-obj.interruptChan:
|
||||
// TODO: should we bubble up the error from Close?
|
||||
// TODO: do we need a mutex around this Close?
|
||||
obj.server.Close() // kill it quickly!
|
||||
case <-shutdownChan:
|
||||
// let this exit
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(closeSignal)
|
||||
|
||||
err := obj.server.Serve(obj.conn) // blocks until Shutdown() is called!
|
||||
if err == nil || err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
// if this returned on its own, then closeSignal can be used...
|
||||
closeError = errwrap.Wrapf(err, "the server errored")
|
||||
}()
|
||||
|
||||
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
|
||||
// immediately return ErrServerClosed. Make sure the program doesn't
|
||||
// exit and waits instead for Shutdown to return.
|
||||
defer func() {
|
||||
defer close(shutdownChan) // signal that shutdown is finished
|
||||
ctx := context.Background()
|
||||
if i := obj.getShutdownTimeout(); i != nil && *i > 0 {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithTimeout(ctx, time.Duration(*i)*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
err := obj.server.Shutdown(ctx) // shutdown gracefully
|
||||
if err == context.DeadlineExceeded {
|
||||
// TODO: should we bubble up the error from Close?
|
||||
// TODO: do we need a mutex around this Close?
|
||||
obj.server.Close() // kill it now
|
||||
}
|
||||
}()
|
||||
|
||||
startupChan := make(chan struct{})
|
||||
close(startupChan) // send one initial signal
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Looping...")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-startupChan:
|
||||
startupChan = nil
|
||||
send = true
|
||||
|
||||
case <-closeSignal: // something shut us down early
|
||||
return closeError
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||
// It does however check that certain runtime requirements (such as the Root dir
|
||||
// existing if one was specified) are fulfilled.
|
||||
func (obj *HTTPServerRes) CheckApply(apply bool) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("CheckApply")
|
||||
}
|
||||
|
||||
// XXX: We don't want the initial CheckApply to return true until the
|
||||
// Watch has started up, so we must block here until that's the case...
|
||||
|
||||
// Cheap runtime validation!
|
||||
if obj.Root != "" {
|
||||
fileInfo, err := os.Stat(obj.Root)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "can't stat Root dir")
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
return false, fmt.Errorf("the Root path is not a dir")
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil // always succeeds, with nothing to do!
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HTTPServerRes) Cmp(r engine.Res) error {
|
||||
// we can only compare HTTPServerRes to others of the same resource kind
|
||||
res, ok := r.(*HTTPServerRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.Address != res.Address {
|
||||
return fmt.Errorf("the Address differs")
|
||||
}
|
||||
|
||||
if (obj.Timeout == nil) != (res.Timeout == nil) { // xor
|
||||
return fmt.Errorf("the Timeout differs")
|
||||
}
|
||||
if obj.Timeout != nil && res.Timeout != nil {
|
||||
if *obj.Timeout != *res.Timeout { // compare the values
|
||||
return fmt.Errorf("the value of Timeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.ReadTimeout == nil) != (res.ReadTimeout == nil) {
|
||||
return fmt.Errorf("the ReadTimeout differs")
|
||||
}
|
||||
if obj.ReadTimeout != nil && res.ReadTimeout != nil {
|
||||
if *obj.ReadTimeout != *res.ReadTimeout {
|
||||
return fmt.Errorf("the value of ReadTimeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.WriteTimeout == nil) != (res.WriteTimeout == nil) {
|
||||
return fmt.Errorf("the WriteTimeout differs")
|
||||
}
|
||||
if obj.WriteTimeout != nil && res.WriteTimeout != nil {
|
||||
if *obj.WriteTimeout != *res.WriteTimeout {
|
||||
return fmt.Errorf("the value of WriteTimeout differs")
|
||||
}
|
||||
}
|
||||
if (obj.ShutdownTimeout == nil) != (res.ShutdownTimeout == nil) {
|
||||
return fmt.Errorf("the ShutdownTimeout differs")
|
||||
}
|
||||
if obj.ShutdownTimeout != nil && res.ShutdownTimeout != nil {
|
||||
if *obj.ShutdownTimeout != *res.ShutdownTimeout {
|
||||
return fmt.Errorf("the value of ShutdownTimeout differs")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: We could do this sort of thing to skip checking Timeout when it
|
||||
// is not used, but for the moment, this is overkill and not needed yet.
|
||||
//a := obj.ReadTimeout != nil
|
||||
//b := obj.WriteTimeout != nil
|
||||
//c := obj.ShutdownTimeout != nil
|
||||
//if !(obj.Timeout != nil && (a && b && c)) {
|
||||
// // the Timeout param is not being ignored
|
||||
//}
|
||||
|
||||
if obj.Root != res.Root {
|
||||
return fmt.Errorf("the Root differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Interrupt is called to ask the execution of this resource to end early. It
|
||||
// will cause the server Shutdown to end abruptly instead of leading open client
|
||||
// connections terminate gracefully. It does this by causing the server Close
|
||||
// method to run.
|
||||
func (obj *HTTPServerRes) Interrupt() error {
|
||||
close(obj.interruptChan) // this should cause obj.server.Close() to run!
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||
// TODO: should this copy internal state?
|
||||
func (obj *HTTPServerRes) Copy() engine.CopyableRes {
|
||||
var timeout, readTimeout, writeTimeout, shutdownTimeout *uint64
|
||||
if obj.Timeout != nil {
|
||||
x := *obj.Timeout
|
||||
timeout = &x
|
||||
}
|
||||
if obj.ReadTimeout != nil {
|
||||
x := *obj.ReadTimeout
|
||||
readTimeout = &x
|
||||
}
|
||||
if obj.WriteTimeout != nil {
|
||||
x := *obj.WriteTimeout
|
||||
writeTimeout = &x
|
||||
}
|
||||
if obj.ShutdownTimeout != nil {
|
||||
x := *obj.ShutdownTimeout
|
||||
shutdownTimeout = &x
|
||||
}
|
||||
return &HTTPServerRes{
|
||||
Address: obj.Address,
|
||||
Timeout: timeout,
|
||||
ReadTimeout: readTimeout,
|
||||
WriteTimeout: writeTimeout,
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
Root: obj.Root,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *HTTPServerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes HTTPServerRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*HTTPServerRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to HTTPServerRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = HTTPServerRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not. Can
|
||||
// these two resources be merged, aka, does this resource support doing so? Will
|
||||
// resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *HTTPServerRes) GroupCmp(r engine.GroupableRes) error {
|
||||
res1, ok1 := r.(*HTTPFileRes) // different from what we usually do!
|
||||
if ok1 {
|
||||
// If the http file resource has the Server field specified,
|
||||
// then it must match against our name field if we want it to
|
||||
// group with us.
|
||||
if res1.Server != "" && res1.Server != obj.Name() {
|
||||
return fmt.Errorf("resource groups with a different server name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("resource is not the right kind")
|
||||
}
|
||||
|
||||
// readHandler handles all the incoming download requests from clients.
|
||||
func (obj *HTTPServerRes) handler() func(http.ResponseWriter, *http.Request) {
|
||||
// TODO: we could statically pre-compute some stuff here...
|
||||
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Client: %s", req.RemoteAddr)
|
||||
}
|
||||
// TODO: would this leak anything security sensitive in our log?
|
||||
obj.init.Logf("URL: %s", req.URL)
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Path: %s", req.URL.Path)
|
||||
}
|
||||
|
||||
// We only allow GET at the moment.
|
||||
if req.Method != http.MethodGet {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
requestPath := req.URL.Path // TODO: is this what we want here?
|
||||
|
||||
//var handle io.Reader // TODO: simplify?
|
||||
var handle io.ReadSeeker
|
||||
|
||||
// Look through the autogrouped resources!
|
||||
// TODO: can we improve performance by only searching here once?
|
||||
for _, x := range obj.GetGroup() { // grouped elements
|
||||
res, ok := x.(*HTTPFileRes) // convert from Res
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if requestPath != res.getPath() {
|
||||
continue // not me
|
||||
}
|
||||
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Got grouped file: %s", res.String())
|
||||
}
|
||||
var err error
|
||||
handle, err = res.getContent()
|
||||
if err != nil {
|
||||
obj.init.Logf("could not get content for: %s", requestPath)
|
||||
msg, httpStatus := toHTTPError(err)
|
||||
http.Error(w, msg, httpStatus)
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Look in root if we have one, and we haven't got a file yet...
|
||||
if obj.Root != "" && handle == nil {
|
||||
|
||||
p := filepath.Join(obj.Root, requestPath) // normal unsafe!
|
||||
if !strings.HasPrefix(p, obj.Root) { // root ends with /
|
||||
// user might have tried a ../../etc/passwd hack
|
||||
obj.init.Logf("join inconsistency: %s", p)
|
||||
http.NotFound(w, req) // lie to them...
|
||||
return
|
||||
}
|
||||
if HTTPUseSecureJoin {
|
||||
var err error
|
||||
p, err = securefilepath.SecureJoin(obj.Root, requestPath)
|
||||
if err != nil {
|
||||
obj.init.Logf("secure join fail: %s", p)
|
||||
http.NotFound(w, req) // lie to them...
|
||||
return
|
||||
}
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Got file at root: %s", p)
|
||||
}
|
||||
var err error
|
||||
handle, err = os.Open(p)
|
||||
if err != nil {
|
||||
obj.init.Logf("could not open: %s", p)
|
||||
msg, httpStatus := toHTTPError(err)
|
||||
http.Error(w, msg, httpStatus)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// We never found a file...
|
||||
if handle == nil {
|
||||
if obj.init.Debug || true { // XXX: maybe we should always do this?
|
||||
obj.init.Logf("File not found: %s", requestPath)
|
||||
}
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the last-modified time if we can.
|
||||
modtime := time.Now()
|
||||
if f, ok := handle.(*os.File); ok {
|
||||
fi, err := f.Stat()
|
||||
if err == nil {
|
||||
modtime = fi.ModTime()
|
||||
}
|
||||
// TODO: if Stat errors, should we fail the whole thing?
|
||||
}
|
||||
|
||||
// XXX: is requestPath what we want for the name field?
|
||||
http.ServeContent(w, req, requestPath, modtime, handle)
|
||||
//obj.init.Logf("%d bytes sent", n) // XXX: how do we know (on the server-side) if it worked?
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPFileRes is a file that exists within an http server. The name is used as
|
||||
// the public path of the file, unless the filename field is specified, and in
|
||||
// that case it is used instead. The way this works is that it autogroups at
|
||||
// runtime with an existing http resource, and in doing so makes the file
|
||||
// associated with this resource available for serving from that http server.
|
||||
type HTTPFileRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable // XXX: add autoedge support
|
||||
traits.Groupable // can be grouped into HTTPServerRes
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Server is the name of the http server resource to group this into. If
|
||||
// it is omitted, and there is only a single http resource, then it will
|
||||
// be grouped into it automatically. If there is more than one main http
|
||||
// resource being used, then the grouping behaviour is *undefined* when
|
||||
// this is not specified, and it is not recommended to leave this blank!
|
||||
Server string `lang:"server" yaml:"server"`
|
||||
|
||||
// Filename is the name of the file this data should appear as on the
|
||||
// http server.
|
||||
Filename string `lang:"filename" yaml:"filename"`
|
||||
|
||||
// Path is the absolute path to a file that should be used as the source
|
||||
// for this file resource. It must not be combined with the data field.
|
||||
Path string `lang:"path" yaml:"path"`
|
||||
|
||||
// Data is the file content that should be used as the source for this
|
||||
// file resource. It must not be combined with the path field.
|
||||
// TODO: should this be []byte instead?
|
||||
Data string `lang:"data" yaml:"data"`
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HTTPFileRes) Default() engine.Res {
|
||||
return &HTTPFileRes{}
|
||||
}
|
||||
|
||||
// getPath returns the actual path we respond to. When Filename is not
|
||||
// specified, we use the Name. Note that this is the filename that will be seen
|
||||
// on the http server, it is *not* the source path to the actual file contents
|
||||
// being sent by the server.
|
||||
func (obj *HTTPFileRes) getPath() string {
|
||||
if obj.Filename != "" {
|
||||
return obj.Filename
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// getContent returns the content that we expect from this resource. It depends
|
||||
// on whether the user specified the Path or Data fields, and whether the Path
|
||||
// exists or not.
|
||||
func (obj *HTTPFileRes) getContent() (io.ReadSeeker, error) {
|
||||
if obj.Path != "" && obj.Data != "" {
|
||||
// programming error! this should have been caught in Validate!
|
||||
return nil, fmt.Errorf("must not specify Path and Data")
|
||||
}
|
||||
|
||||
if obj.Path != "" {
|
||||
return os.Open(obj.Path)
|
||||
}
|
||||
|
||||
return bytes.NewReader([]byte(obj.Data)), nil
|
||||
}
|
||||
|
||||
// Validate checks if the resource data structure was populated correctly.
|
||||
func (obj *HTTPFileRes) Validate() error {
|
||||
if obj.getPath() == "" {
|
||||
return fmt.Errorf("empty filename")
|
||||
}
|
||||
// FIXME: does getPath need to start with a slash?
|
||||
|
||||
if obj.Path != "" && !strings.HasPrefix(obj.Path, "/") {
|
||||
return fmt.Errorf("the Path must be absolute")
|
||||
}
|
||||
|
||||
if obj.Path != "" && obj.Data != "" {
|
||||
return fmt.Errorf("must not specify Path and Data")
|
||||
}
|
||||
|
||||
// NOTE: if obj.Path == "" && obj.Data == "" then we have an empty file!
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *HTTPFileRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *HTTPFileRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events. This
|
||||
// particular one does absolutely nothing but block until we've received a done
|
||||
// signal.
|
||||
func (obj *HTTPFileRes) Watch() error {
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
select {
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckApply never has anything to do for this resource, so it always succeeds.
|
||||
func (obj *HTTPFileRes) CheckApply(apply bool) (bool, error) {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("CheckApply")
|
||||
}
|
||||
|
||||
return true, nil // always succeeds, with nothing to do!
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HTTPFileRes) Cmp(r engine.Res) error {
|
||||
// we can only compare HTTPFileRes to others of the same resource kind
|
||||
res, ok := r.(*HTTPFileRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.Server != res.Server {
|
||||
return fmt.Errorf("the Server field differs")
|
||||
}
|
||||
if obj.Filename != res.Filename {
|
||||
return fmt.Errorf("the Filename differs")
|
||||
}
|
||||
if obj.Path != res.Path {
|
||||
return fmt.Errorf("the Path differs")
|
||||
}
|
||||
if obj.Data != res.Data {
|
||||
return fmt.Errorf("the Data differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *HTTPFileRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes HTTPFileRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*HTTPFileRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to HTTPFileRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = HTTPFileRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// toHTTPError returns a non-specific HTTP error message and status code for a
|
||||
// given non-nil error value. It's important that toHTTPError does not actually
|
||||
// return err.Error(), since msg and httpStatus are returned to users, and
|
||||
// historically Go's ServeContent always returned just "404 Not Found" for all
|
||||
// errors. We don't want to start leaking information in error messages.
|
||||
// NOTE: This was copied and modified slightly from the golang net/http package.
|
||||
// See: https://github.com/golang/go/issues/38375
|
||||
func toHTTPError(err error) (msg string, httpStatus int) {
|
||||
if os.IsNotExist(err) {
|
||||
//return "404 page not found", http.StatusNotFound
|
||||
return http.StatusText(http.StatusNotFound), http.StatusNotFound
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
//return "403 Forbidden", http.StatusForbidden
|
||||
return http.StatusText(http.StatusForbidden), http.StatusForbidden
|
||||
}
|
||||
// Default:
|
||||
//return "500 Internal Server Error", http.StatusInternalServerError
|
||||
return http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,26 +18,37 @@
|
||||
package resources
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("kv", func() Res { return &KVRes{} })
|
||||
engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
|
||||
}
|
||||
|
||||
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
|
||||
// KVResSkipCmpStyle represents the different styles of comparison when using
|
||||
// SkipLessThan.
|
||||
type KVResSkipCmpStyle int
|
||||
|
||||
// These are the different allowed comparison styles. Most folks will want SkipCmpStyleInt.
|
||||
// These are the different allowed comparison styles. Most folks will want
|
||||
// SkipCmpStyleInt.
|
||||
const (
|
||||
SkipCmpStyleInt KVResSkipCmpStyle = iota
|
||||
SkipCmpStyleString
|
||||
)
|
||||
|
||||
const (
|
||||
kvCheckApplyTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// KVRes is a resource which writes a key/value pair into cluster wide storage.
|
||||
// It will ensure that the key is set to the requested value. The one exception
|
||||
// is that if you use the SkipLessThan parameter, then it will only replace the
|
||||
@@ -47,29 +58,47 @@ const (
|
||||
// The one exception is that when this resource receives a refresh signal, then
|
||||
// it will set the value to be the exact one if they are not identical already.
|
||||
type KVRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
// XXX: shouldn't the name be the key?
|
||||
Key string `yaml:"key"` // key to set
|
||||
Value *string `yaml:"value"` // value to set (nil to delete)
|
||||
SkipLessThan bool `yaml:"skiplessthan"` // skip updates as long as stored value is greater
|
||||
SkipCmpStyle KVResSkipCmpStyle `yaml:"skipcmpstyle"` // how to do the less than cmp
|
||||
traits.Base // add the base methods without re-implementation
|
||||
//traits.Groupable // TODO: it could be useful to group our writes and watches!
|
||||
traits.Refreshable
|
||||
traits.Recvable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Key represents the key to set. If it is not specified, the Name value
|
||||
// is used instead.
|
||||
Key string `lang:"key" yaml:"key"`
|
||||
// Value represents the string value to set. If this value is nil or,
|
||||
// undefined, then this will delete that key.
|
||||
Value *string `lang:"value" yaml:"value"`
|
||||
// SkipLessThan causes the value to be updated as long as it is greater.
|
||||
SkipLessThan bool `lang:"skiplessthan" yaml:"skiplessthan"`
|
||||
// SkipCmpStyle is the type of compare function used when determining if
|
||||
// the value is greater when using the SkipLessThan parameter.
|
||||
SkipCmpStyle KVResSkipCmpStyle `lang:"skipcmpstyle" yaml:"skipcmpstyle"`
|
||||
|
||||
interruptChan chan struct{}
|
||||
|
||||
// TODO: does it make sense to have different backends here? (eg: local)
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *KVRes) Default() Res {
|
||||
return &KVRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
// getKey returns the key to be used for this resource. If the Key field is
|
||||
// specified, it will use that, otherwise it uses the Name.
|
||||
func (obj *KVRes) getKey() string {
|
||||
if obj.Key != "" {
|
||||
return obj.Key
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *KVRes) Default() engine.Res {
|
||||
return &KVRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
// FIXME: This will catch most issues unless data is passed in after Init with
|
||||
// the Send/Recv mechanism. Should the engine re-call Validate after Send/Recv?
|
||||
func (obj *KVRes) Validate() error {
|
||||
if obj.Key == "" {
|
||||
if obj.getKey() == "" {
|
||||
return fmt.Errorf("key must not be empty")
|
||||
}
|
||||
if obj.SkipLessThan {
|
||||
@@ -83,26 +112,38 @@ func (obj *KVRes) Validate() error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the resource.
|
||||
func (obj *KVRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *KVRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
obj.interruptChan = make(chan struct{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *KVRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *KVRes) Watch() error {
|
||||
// FIXME: add timeout to context
|
||||
// The obj.init.Done channel is closed by the engine to signal shutdown.
|
||||
ctx, cancel := util.ContextWithCloser(context.Background(), obj.init.Done)
|
||||
defer cancel()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
ch, err := obj.init.World.StrMapWatch(ctx, obj.getKey()) // get possible events!
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ch := obj.Data().World.StrMapWatch(obj.Key) // get possible events!
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
// NOTE: this part is very similar to the file resource code
|
||||
@@ -113,36 +154,31 @@ func (obj *KVRes) Watch() error {
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.Data().Debug {
|
||||
log.Printf("%s: Event!", obj)
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("event!")
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lessThanCheck checks for less than validity.
|
||||
func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
|
||||
|
||||
func (obj *KVRes) lessThanCheck(value string) (bool, error) {
|
||||
v := *obj.Value
|
||||
if value == v { // redundant check for safety
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var refresh = obj.Refresh() // do we have a pending reload to apply?
|
||||
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||
if !obj.SkipLessThan || refresh { // update lessthan on refresh
|
||||
return false, nil
|
||||
}
|
||||
@@ -174,16 +210,31 @@ func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
// CheckApply method for Password resource. Does nothing, returns happy!
|
||||
func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply(%t)", obj, apply)
|
||||
func (obj *KVRes) CheckApply(apply bool) (bool, error) {
|
||||
obj.init.Logf("CheckApply(%t)", apply)
|
||||
|
||||
if val, exists := obj.Recv["Value"]; exists && val.Changed {
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait() // this must be above the defer cancel() call
|
||||
ctx, cancel := context.WithTimeout(context.Background(), kvCheckApplyTimeout)
|
||||
defer cancel()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
select {
|
||||
case <-obj.interruptChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// let this exit
|
||||
}
|
||||
}()
|
||||
|
||||
if val, exists := obj.init.Recv()["Value"]; exists && val.Changed {
|
||||
// if we received on Value, and it changed, wooo, nothing to do.
|
||||
log.Printf("CheckApply: `Value` was updated!")
|
||||
obj.init.Logf("CheckApply: `Value` was updated!")
|
||||
}
|
||||
|
||||
hostname := obj.Data().Hostname // me
|
||||
keyMap, err := obj.Data().World.StrMapGet(obj.Key)
|
||||
hostname := obj.init.Hostname // me
|
||||
keyMap, err := obj.init.World.StrMapGet(ctx, obj.getKey())
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "check error during StrGet")
|
||||
}
|
||||
@@ -203,7 +254,7 @@ func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return true, nil // nothing to delete, we're good!
|
||||
|
||||
} else if ok && obj.Value == nil { // delete
|
||||
err := obj.Data().World.StrMapDel(obj.Key)
|
||||
err := obj.init.World.StrMapDel(ctx, obj.getKey())
|
||||
return false, errwrap.Wrapf(err, "apply error during StrDel")
|
||||
}
|
||||
|
||||
@@ -211,73 +262,66 @@ func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := obj.Data().World.StrMapSet(obj.Key, *obj.Value); err != nil {
|
||||
if err := obj.init.World.StrMapSet(ctx, obj.getKey(), *obj.Value); err != nil {
|
||||
return false, errwrap.Wrapf(err, "apply error during StrSet")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// KVUID is the UID struct for KVRes.
|
||||
type KVUID struct {
|
||||
BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *KVRes) UIDs() []ResUID {
|
||||
x := &KVUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *KVRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*KVRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return false // TODO: this is doable!
|
||||
// TODO: it could be useful to group our writes and watches!
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *KVRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *KVRes) Cmp(r engine.Res) error {
|
||||
// we can only compare KVRes to others of the same resource kind
|
||||
res, ok := r.(*KVRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Key != res.Key {
|
||||
return false
|
||||
if obj.getKey() != res.getKey() {
|
||||
return fmt.Errorf("the Key differs")
|
||||
}
|
||||
if (obj.Value == nil) != (res.Value == nil) { // xor
|
||||
return false
|
||||
return fmt.Errorf("the Value differs")
|
||||
}
|
||||
if obj.Value != nil && res.Value != nil {
|
||||
if *obj.Value != *res.Value { // compare the strings
|
||||
return false
|
||||
return fmt.Errorf("the contents of Value differs")
|
||||
}
|
||||
}
|
||||
if obj.SkipLessThan != res.SkipLessThan {
|
||||
return false
|
||||
return fmt.Errorf("the SkipLessThan param differs")
|
||||
}
|
||||
if obj.SkipCmpStyle != res.SkipCmpStyle {
|
||||
return false
|
||||
return fmt.Errorf("the SkipCmpStyle param differs")
|
||||
}
|
||||
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// Interrupt is called to ask the execution of this resource to end early.
|
||||
func (obj *KVRes) Interrupt() error {
|
||||
close(obj.interruptChan)
|
||||
return nil
|
||||
}
|
||||
|
||||
// KVUID is the UID struct for KVRes.
|
||||
type KVUID struct {
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *KVRes) UIDs() []engine.ResUID {
|
||||
x := &KVUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes KVRes // indirection to avoid infinite recursion
|
||||
|
||||
728
engine/resources/mount.go
Normal file
728
engine/resources/mount.go
Normal file
@@ -0,0 +1,728 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
sdbus "github.com/coreos/go-systemd/dbus"
|
||||
"github.com/coreos/go-systemd/unit"
|
||||
systemdUtil "github.com/coreos/go-systemd/util"
|
||||
fstab "github.com/deniswernert/go-fstab"
|
||||
"github.com/godbus/dbus"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("mount", func() engine.Res { return &MountRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
// procFilesystems is a file that lists all the valid filesystem types.
|
||||
procFilesystems = "/proc/filesystems"
|
||||
// procPath is the path to /proc/mounts which contains all active mounts.
|
||||
procPath = "/proc/mounts"
|
||||
// fstabPath is the path to the fstab file which defines mounts.
|
||||
fstabPath = "/etc/fstab"
|
||||
// fstabUmask is the umask (permissions) used to edit /etc/fstab.
|
||||
fstabUmask = 0644
|
||||
|
||||
// getStatus64 is an ioctl command to get the status of file backed
|
||||
// loopback devices (i.e. iso file mounts.)
|
||||
getStatus64 = 0x4C05
|
||||
// loopFileUmask is the umask (permissions) used to read the loop file.
|
||||
loopFileUmask = 0660
|
||||
|
||||
// devDisk is the path where disks and partitions can be found, organized
|
||||
// by uuid/label/path.
|
||||
devDisk = "/dev/disk/"
|
||||
// diskByUUID is the location of symlinks for devices by UUID.
|
||||
diskByUUID = devDisk + "by-uuid/"
|
||||
// diskByLabel is the location of symlinks for devices by label.
|
||||
diskByLabel = devDisk + "by-label/"
|
||||
// diskByUUID is the location of symlinks for partitions by UUID.
|
||||
diskByPartUUID = devDisk + "by-partuuid/"
|
||||
// diskByLabel is the location of symlinks for partitions by label.
|
||||
diskByPartLabel = devDisk + "by-partlabel/"
|
||||
|
||||
// dbusSystemdService is the service to connect to systemd itself.
|
||||
dbusSystemd1Service = "org.freedesktop.systemd1"
|
||||
// dbusSystemd1Interface is the base systemd1 path.
|
||||
dbusSystemd1Path = "/org/freedesktop/systemd1"
|
||||
// dbusUnitPath is the dbus path where mount unit files are found.
|
||||
dbusUnitPath = dbusSystemd1Path + "/unit/"
|
||||
// dbusSystemd1Interface is the base systemd1 interface.
|
||||
dbusSystemd1Interface = "org.freedesktop.systemd1"
|
||||
// dbusMountInterface is used as an argument to filter dbus messages.
|
||||
dbusMountInterface = dbusSystemd1Interface + ".Mount"
|
||||
// dbusManagerInterface is the systemd manager interface used for
|
||||
// interfacing with systemd units.
|
||||
dbusManagerInterface = dbusSystemd1Interface + ".Manager"
|
||||
// dbusRestartUnit is the dbus method for restarting systemd units.
|
||||
dbusRestartUnit = dbusManagerInterface + ".RestartUnit"
|
||||
// dbusReloadSystemd is the dbus method for reloading systemd settings.
|
||||
// (i.e. systemctl daemon-reload)
|
||||
dbusReloadSystemd = dbusManagerInterface + ".Reload"
|
||||
// restartTimeout is the delay before restartUnit is assumed to have
|
||||
// failed.
|
||||
dbusRestartCtxTimeout = 10
|
||||
// dbusSignalJobRemoved is the name of the dbus signal that produces a
|
||||
// message when a dbus job is done (or has errored.)
|
||||
dbusSignalJobRemoved = "JobRemoved"
|
||||
)
|
||||
|
||||
// MountRes is a systemd mount resource that adds/removes entries from
|
||||
// /etc/fstab, and makes sure the defined device is mounted or unmounted
|
||||
// accordingly. The mount point is set according to the resource's name.
|
||||
type MountRes struct {
|
||||
traits.Base
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// State must be exists ot absent. If absent, remaining fields are ignored.
|
||||
State string `yaml:"state"`
|
||||
Device string `yaml:"device"` // location of the device or image
|
||||
Type string `yaml:"type"` // the type of filesystem
|
||||
Options map[string]string `yaml:"options"` // mount options
|
||||
Freq int `yaml:"freq"` // dump frequency
|
||||
PassNo int `yaml:"passno"` // verification order
|
||||
|
||||
mount *fstab.Mount // struct representing the mount
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *MountRes) Default() engine.Res {
|
||||
return &MountRes{
|
||||
Options: defaultMntOps(),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *MountRes) Validate() error {
|
||||
var err error
|
||||
|
||||
// validate state
|
||||
if obj.State != "exists" && obj.State != "absent" {
|
||||
return fmt.Errorf("state must be 'exists', or 'absent'")
|
||||
}
|
||||
|
||||
// validate type
|
||||
fs, err := ioutil.ReadFile(procFilesystems)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error reading %s", procFilesystems)
|
||||
}
|
||||
fsSlice := strings.Fields(string(fs))
|
||||
for i, x := range fsSlice {
|
||||
if x == "nodev" {
|
||||
fsSlice = append(fsSlice[:i], fsSlice[i+1:]...)
|
||||
}
|
||||
}
|
||||
if obj.State != "absent" && !util.StrInList(obj.Type, fsSlice) {
|
||||
return fmt.Errorf("type must be a valid filesystem type (see /proc/filesystems)")
|
||||
}
|
||||
|
||||
// validate mountpoint
|
||||
if strings.Contains(obj.Name(), "//") {
|
||||
return fmt.Errorf("double slashes are not allowed in resource name")
|
||||
}
|
||||
if err := unix.Access(obj.Name(), unix.R_OK); err != nil {
|
||||
return errwrap.Wrapf(err, "error validating mount point: %s", obj.Name())
|
||||
}
|
||||
|
||||
// validate device
|
||||
device, err := evalSpec(obj.Device) // eval symlink
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error evaluating spec: %s", obj.Device)
|
||||
}
|
||||
if err := unix.Access(device, unix.R_OK); err != nil {
|
||||
return errwrap.Wrapf(err, "error validating device: %s", device)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *MountRes) Init(init *engine.Init) error {
|
||||
obj.init = init //save for later
|
||||
|
||||
obj.mount = &fstab.Mount{
|
||||
Spec: obj.Device,
|
||||
File: obj.Name(),
|
||||
VfsType: obj.Type,
|
||||
MntOps: obj.Options,
|
||||
Freq: obj.Freq,
|
||||
PassNo: obj.PassNo,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *MountRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch listens for signals from the mount unit associated with the resource.
|
||||
// It also watch for changes to /etc/fstab, where mounts are defined.
|
||||
func (obj *MountRes) Watch() error {
|
||||
// make sure systemd is running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return fmt.Errorf("systemd is not running")
|
||||
}
|
||||
|
||||
// establish a godbus connection
|
||||
conn, err := util.SystemBusPrivateUsable()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error establishing dbus connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// add a dbus rule to watch signals from the mount unit.
|
||||
args := fmt.Sprintf("type='signal', path='%s', arg0='%s'",
|
||||
dbusUnitPath+sdbus.PathBusEscape(unit.UnitNamePathEscape((obj.Name()+".mount"))),
|
||||
dbusMountInterface,
|
||||
)
|
||||
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
return errwrap.Wrapf(call.Err, "error creating dbus call")
|
||||
}
|
||||
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||
|
||||
ch := make(chan *dbus.Signal)
|
||||
defer close(ch)
|
||||
|
||||
conn.Signal(ch)
|
||||
defer conn.RemoveSignal(ch)
|
||||
|
||||
// watch the fstab file
|
||||
recWatcher, err := recwatch.NewRecWatcher(fstabPath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// close the recwatcher when we're done
|
||||
defer recWatcher.Close()
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send bool
|
||||
var done bool
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-recWatcher.Events():
|
||||
if !ok {
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
done = true
|
||||
continue
|
||||
}
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "unknown recwatcher error")
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
|
||||
send = true
|
||||
|
||||
case event, ok := <-ch:
|
||||
if !ok {
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
done = true
|
||||
continue
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("event: %+v", event)
|
||||
}
|
||||
|
||||
send = true
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fstabCheckApply checks /etc/fstab for entries corresponding to the resource
|
||||
// definition, and adds or deletes the entry as needed.
|
||||
func (obj *MountRes) fstabCheckApply(apply bool) (bool, error) {
|
||||
exists, err := fstabEntryExists(fstabPath, obj.mount)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking if fstab entry exists")
|
||||
}
|
||||
|
||||
// if everything is as it should be, we're done
|
||||
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("fstabCheckApply(%t)", apply)
|
||||
|
||||
if obj.State == "exists" {
|
||||
if err := obj.fstabEntryAdd(fstabPath, obj.mount); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error adding fstab entry: %+v", obj.mount)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
if err := obj.fstabEntryRemove(fstabPath, obj.mount); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error removing fstab entry: %+v", obj.mount)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// mountCheckApply checks if the defined resource is mounted, and mounts or
|
||||
// unmounts it according to the defined state.
|
||||
func (obj *MountRes) mountCheckApply(apply bool) (bool, error) {
|
||||
exists, err := mountExists(procPath, obj.mount)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking if mount exists")
|
||||
}
|
||||
|
||||
// if everything is as it should be, we're done
|
||||
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("mountCheckApply(%t)", apply)
|
||||
|
||||
if obj.State == "exists" {
|
||||
// Reload mounts from /etc/fstab by performing a `daemon-reload` and
|
||||
// restarting `local-fs.target` and `remote-fs.target` units.
|
||||
if err := mountReload(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error reloading /etc/fstab")
|
||||
}
|
||||
return false, nil // we're done
|
||||
}
|
||||
// unmount the device
|
||||
if err := unix.Unmount(obj.Name(), 0); err != nil { // 0 means no flags
|
||||
return false, errwrap.Wrapf(err, "error unmounting %s", obj.Name())
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *MountRes) CheckApply(apply bool) (bool, error) {
|
||||
checkOK := true
|
||||
|
||||
if c, err := obj.fstabCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
if c, err := obj.mountCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and return if they are equivalent.
|
||||
func (obj *MountRes) Cmp(r engine.Res) error {
|
||||
// we can only compare MountRes to others of the same resource kind
|
||||
res, ok := r.(*MountRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
if obj.Type != res.Type {
|
||||
return fmt.Errorf("the Type differs")
|
||||
}
|
||||
if !strMapEq(obj.Options, res.Options) {
|
||||
return fmt.Errorf("the Options differ")
|
||||
}
|
||||
if obj.Freq != res.Freq {
|
||||
return fmt.Errorf("the Type differs")
|
||||
}
|
||||
if obj.PassNo != res.PassNo {
|
||||
return fmt.Errorf("the PassNo differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MountUID is a unique resource identifier.
|
||||
type MountUID struct {
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *MountUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*MountUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.name == res.name
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one although some resources can return multiple.
|
||||
func (obj *MountRes) UIDs() []engine.ResUID {
|
||||
x := &MountUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes MountRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*MountRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to MountRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = MountRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// defaultMntOps returns a map that sets the default mount options for fstab
|
||||
// mounts.
|
||||
func defaultMntOps() map[string]string {
|
||||
return map[string]string{"defaults": ""}
|
||||
}
|
||||
|
||||
// strMapEq returns true, if and only if the two provided maps are identical.
|
||||
func strMapEq(x, y map[string]string) bool {
|
||||
if len(x) != len(y) {
|
||||
return false
|
||||
}
|
||||
for k, v := range x {
|
||||
if val, ok := x[k]; !ok || v != val {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// fstabEntryExists checks whether or not a given mount exists in the provided
|
||||
// fstab file.
|
||||
func fstabEntryExists(file string, mount *fstab.Mount) (bool, error) {
|
||||
mounts, err := fstab.ParseFile(file)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||
}
|
||||
for _, m := range mounts {
|
||||
if m.Equals(mount) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// fstabEntryAdd adds the given mount to the provided fstab file.
|
||||
func (obj *MountRes) fstabEntryAdd(file string, mount *fstab.Mount) error {
|
||||
mounts, err := fstab.ParseFile(file)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||
}
|
||||
for _, m := range mounts {
|
||||
// if the entry exists, we're done
|
||||
if m.Equals(mount) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// mount does not exist so we need to add it
|
||||
mounts = append(mounts, mount)
|
||||
return obj.fstabWrite(file, mounts)
|
||||
}
|
||||
|
||||
// fstabEntryRemove removes the given mount from the provided fstab file.
|
||||
func (obj *MountRes) fstabEntryRemove(file string, mount *fstab.Mount) error {
|
||||
mounts, err := fstab.ParseFile(file)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||
}
|
||||
for i, m := range mounts {
|
||||
// remove any entry with the defined mountpoint
|
||||
if m.File == mount.File {
|
||||
mounts = append(mounts[:i], mounts[i+1:]...)
|
||||
}
|
||||
}
|
||||
return obj.fstabWrite(file, mounts)
|
||||
}
|
||||
|
||||
// fstabWrite generates an fstab file with the given mounts, and writes them to
|
||||
// the provided fstab file.
|
||||
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
|
||||
// build the file contents
|
||||
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
|
||||
contents = contents + mounts.String() + "\n"
|
||||
// write the file
|
||||
if err := ioutil.WriteFile(file, []byte(contents), fstabUmask); err != nil {
|
||||
return errwrap.Wrapf(err, "error writing fstab file: %s", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountExists returns true, if a given mount exists in the given file
|
||||
// (typically /proc/mounts.)
|
||||
func mountExists(file string, mount *fstab.Mount) (bool, error) {
|
||||
var err error
|
||||
m := *mount // make a copy so we don't change the definition
|
||||
|
||||
// resolve the device's symlink if there is one
|
||||
if m.Spec, err = evalSpec(mount.Spec); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error evaluating spec: %s", mount.Spec)
|
||||
}
|
||||
|
||||
// get all mounts
|
||||
mounts, err := fstab.ParseFile(file)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||
}
|
||||
// check for the defined mount
|
||||
for _, p := range mounts {
|
||||
found, err := mountCompare(&m, p)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "mounts could not be compared: %s and %s", mount.String(), p.String())
|
||||
}
|
||||
if found {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// mountCompare compares two mounts. It is assumed that the first comes from a
|
||||
// resource definition, and the second comes from /proc/mounts. It compares the
|
||||
// two after resolving the loopback device's file path (if necessary,) and
|
||||
// ignores freq and passno, as they may differ between the definition and
|
||||
// /proc/mounts.
|
||||
func mountCompare(def, proc *fstab.Mount) (bool, error) {
|
||||
if def.Equals(proc) {
|
||||
return true, nil
|
||||
}
|
||||
if def.File != proc.File {
|
||||
return false, nil
|
||||
}
|
||||
if def.Spec != "" {
|
||||
procSpec, err := loopFilePath(proc.Spec)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if def.Spec != procSpec {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
if !strMapEq(def.MntOps, defaultMntOps()) && !strMapEq(def.MntOps, proc.MntOps) {
|
||||
return false, nil
|
||||
}
|
||||
if def.VfsType != "" && def.VfsType != proc.VfsType {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// mountReload performs a daemon-reload and restarts fs-local.target and
|
||||
// fs-remote.target, to let systemd mount any new entries in /etc/fstab.
|
||||
func mountReload() error {
|
||||
// establish a godbus connection
|
||||
conn, err := util.SystemBusPrivateUsable()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error establishing dbus connection")
|
||||
}
|
||||
defer conn.Close()
|
||||
// systemctl daemon-reload
|
||||
call := conn.Object(dbusSystemd1Service, dbusSystemd1Path).Call(dbusReloadSystemd, 0)
|
||||
if call.Err != nil {
|
||||
return errwrap.Wrapf(call.Err, "error reloading systemd")
|
||||
}
|
||||
|
||||
// systemctl restart local-fs.target
|
||||
if err := restartUnit(conn, "local-fs.target"); err != nil {
|
||||
return errwrap.Wrapf(err, "error restarting unit")
|
||||
}
|
||||
|
||||
// systemctl restart remote-fs.target
|
||||
if err := restartUnit(conn, "remote-fs.target"); err != nil {
|
||||
return errwrap.Wrapf(err, "error restarting unit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// restartUnit restarts the given dbus unit and waits for it to finish starting
|
||||
// up. If restartTimeout is exceeded, it will return an error.
|
||||
func restartUnit(conn *dbus.Conn, unit string) error {
|
||||
// timeout if we don't get the JobRemoved event
|
||||
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Add a dbus rule to watch the systemd1 JobRemoved signal used to wait
|
||||
// until the restart job completes.
|
||||
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s', arg2='%s'",
|
||||
dbusSystemd1Path,
|
||||
dbusManagerInterface,
|
||||
dbusSignalJobRemoved,
|
||||
unit,
|
||||
)
|
||||
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
return errwrap.Wrapf(call.Err, "error creating dbus call")
|
||||
}
|
||||
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||
|
||||
// channel for godbus connection
|
||||
ch := make(chan *dbus.Signal)
|
||||
defer close(ch)
|
||||
|
||||
conn.Signal(ch)
|
||||
defer conn.RemoveSignal(ch)
|
||||
|
||||
// restart the unit
|
||||
sd1 := conn.Object(dbusSystemd1Service, dbus.ObjectPath(dbusSystemd1Path))
|
||||
if call := sd1.Call(dbusRestartUnit, 0, unit, "fail"); call.Err != nil {
|
||||
return errwrap.Wrapf(call.Err, "error restarting unit: %s", unit)
|
||||
}
|
||||
|
||||
// wait for the job to be removed, indicating completion
|
||||
select {
|
||||
case event, ok := <-ch:
|
||||
if !ok {
|
||||
return fmt.Errorf("channel closed unexpectedly")
|
||||
}
|
||||
if event.Body[3] != "done" {
|
||||
return fmt.Errorf("unexpected job status: %s", event.Body[3])
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return fmt.Errorf("restarting %s failed due to context timeout", unit)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// evalSpec resolves the device from the supplied spec, i.e. it follows the
|
||||
// symlink, if any, from the provided uuid, label, or path.
|
||||
func evalSpec(spec string) (string, error) {
|
||||
var path string
|
||||
m := &fstab.Mount{}
|
||||
m.Spec = spec
|
||||
|
||||
switch m.SpecType() {
|
||||
case fstab.UUID:
|
||||
path = diskByUUID + m.SpecValue()
|
||||
case fstab.Label:
|
||||
path = diskByLabel + m.SpecValue()
|
||||
case fstab.PartUUID:
|
||||
path = diskByPartUUID + m.SpecValue()
|
||||
case fstab.PartLabel:
|
||||
path = diskByPartLabel + m.SpecValue()
|
||||
case fstab.Path:
|
||||
path = m.SpecValue()
|
||||
default:
|
||||
return "", fmt.Errorf("unexpected spec type: %v", m.SpecType())
|
||||
}
|
||||
|
||||
return filepath.EvalSymlinks(path)
|
||||
}
|
||||
|
||||
// loopFilePath returns the file path of the mounted filesystem image, backing
|
||||
// the given loopback device.
|
||||
func loopFilePath(spec string) (string, error) {
|
||||
// if it's not a loopback device, return the input
|
||||
if !strings.Contains(spec, "/dev/loop") {
|
||||
return spec, nil
|
||||
}
|
||||
info, err := getLoopInfo(spec)
|
||||
if err != nil {
|
||||
return "", errwrap.Wrapf(err, "error getting loop info")
|
||||
}
|
||||
// trim the extra null chars off the end of the filename
|
||||
return string(bytes.Trim(info.FileName[:], "\x00")), nil
|
||||
}
|
||||
|
||||
// loopInfo is a datastructure that holds relevant information about a file
|
||||
// backed loopback device. Code is based on freddierice/go-losetup.
|
||||
type loopInfo struct {
|
||||
Device uint64
|
||||
INode uint64
|
||||
RDevice uint64
|
||||
Offset uint64
|
||||
SizeLimit uint64
|
||||
Number uint32
|
||||
EncryptType uint32
|
||||
EncryptKeySize uint32
|
||||
Flags uint32
|
||||
FileName [64]byte
|
||||
CryptName [64]byte
|
||||
EncryptKey [32]byte
|
||||
Init [2]uint64
|
||||
}
|
||||
|
||||
// getLoopInfo returns a loopInfo struct containing information about the
|
||||
// provided file backed loopback device.
|
||||
func getLoopInfo(loop string) (*loopInfo, error) {
|
||||
// open the loop file
|
||||
f, err := os.OpenFile(loop, 0, loopFileUmask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening %s: %s", loop, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// deserialize the contents
|
||||
retInfo := &loopInfo{}
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), getStatus64, uintptr(unsafe.Pointer(retInfo)))
|
||||
if errno == unix.ENXIO {
|
||||
return nil, fmt.Errorf("device not backed by a file")
|
||||
} else if errno != 0 {
|
||||
return nil, fmt.Errorf("error getting info about %s (errno: %d)", loop, errno)
|
||||
}
|
||||
|
||||
return retInfo, nil
|
||||
}
|
||||
76
engine/resources/mount_linux_test.go
Normal file
76
engine/resources/mount_linux_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root !darwin
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
fstab "github.com/deniswernert/go-fstab"
|
||||
)
|
||||
|
||||
func TestMountExists(t *testing.T) {
|
||||
const procMock1 = `/tmp/mount0 /mnt/proctest ext4 rw,seclabel,relatime,data=ordered 0 0` + "\n"
|
||||
|
||||
var mountExistsTests = []struct {
|
||||
procMock []byte
|
||||
in *fstab.Mount
|
||||
out bool
|
||||
}{
|
||||
{
|
||||
[]byte(procMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "/tmp/mount0",
|
||||
File: "/mnt/proctest",
|
||||
VfsType: "ext4",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
file, err := ioutil.TempFile("", "proc")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temp file: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
for _, test := range mountExistsTests {
|
||||
if err := ioutil.WriteFile(file.Name(), test.procMock, 0664); err != nil {
|
||||
t.Errorf("error writing proc file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
if err := ioutil.WriteFile(test.in.Spec, []byte{}, 0664); err != nil {
|
||||
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
result, err := mountExists(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
|
||||
return
|
||||
}
|
||||
if result != test.out {
|
||||
t.Errorf("mountExistsTests test wanted: %t, got: %t", test.out, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
295
engine/resources/mount_test.go
Normal file
295
engine/resources/mount_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
fstab "github.com/deniswernert/go-fstab"
|
||||
)
|
||||
|
||||
const fstabMock1 = `UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad / ext4 defaults 1 1` + "\n"
|
||||
|
||||
var fstabWriteTests = []struct {
|
||||
in fstab.Mounts
|
||||
}{
|
||||
{
|
||||
fstab.Mounts{
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||
File: "/boot",
|
||||
VfsType: "ext3",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 2,
|
||||
},
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/mapper/home",
|
||||
File: "/home",
|
||||
VfsType: "ext3",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
fstab.Mounts{
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/cdrom",
|
||||
File: "/mnt/cdrom",
|
||||
VfsType: "iso9660",
|
||||
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (obj *MountRes) TestFstabWrite(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "fstab")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temp file: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
for _, test := range fstabWriteTests {
|
||||
if err := obj.fstabWrite(file.Name(), test.in); err != nil {
|
||||
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
for _, mount := range test.in {
|
||||
exists, err := fstabEntryExists(file.Name(), mount)
|
||||
if err != nil {
|
||||
t.Errorf("error checking if fstab entry %s exists: %v", mount.String(), err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("failed to write %s to fstab", mount.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fstabEntryAddTests = []struct {
|
||||
fstabMock []byte
|
||||
in *fstab.Mount
|
||||
}{
|
||||
{
|
||||
[]byte(fstabMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/sdb1",
|
||||
File: "/mnt/foo",
|
||||
VfsType: "ext2",
|
||||
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
|
||||
},
|
||||
},
|
||||
{
|
||||
[]byte(fstabMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||
File: "/",
|
||||
VfsType: "ext3",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (obj *MountRes) TestFstabEntryAdd(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "fstab")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temp file: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
for _, test := range fstabEntryAddTests {
|
||||
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
err := obj.fstabEntryAdd(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error adding fstab entry: %s to file: %s: %v", test.in.String(), file.Name(), err)
|
||||
return
|
||||
}
|
||||
exists, err := fstabEntryExists(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("fstab failed to add entry: %s to fstab", test.in.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fstabEntryRemoveTests = []struct {
|
||||
fstabMock []byte
|
||||
in *fstab.Mount
|
||||
}{
|
||||
{
|
||||
[]byte(fstabMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
|
||||
File: "/",
|
||||
VfsType: "ext4",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func (obj *MountRes) TestFstabEntryRemove(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "fstab")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temp file: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
for _, test := range fstabEntryRemoveTests {
|
||||
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
err := obj.fstabEntryRemove(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error removing fstab entry: %s from file: %s: %v", test.in.String(), file.Name(), err)
|
||||
return
|
||||
}
|
||||
exists, err := fstabEntryExists(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
t.Errorf("fstab failed to remove entry: %s from fstab", test.in.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var mountCompareTests = []struct {
|
||||
dIn *fstab.Mount
|
||||
pIn *fstab.Mount
|
||||
out bool
|
||||
}{
|
||||
{
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/foo",
|
||||
File: "/mnt/foo",
|
||||
VfsType: "ext3",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
},
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/foo",
|
||||
File: "/mnt/foo",
|
||||
VfsType: "ext3",
|
||||
MntOps: map[string]string{"foo": "bar", "baz": ""},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||
File: "/mnt/foo",
|
||||
VfsType: "ext3",
|
||||
},
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||
File: "/mnt/bar",
|
||||
VfsType: "ext3",
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
var fstabEntryExistsTests = []struct {
|
||||
fstabMock []byte
|
||||
in *fstab.Mount
|
||||
out bool
|
||||
}{
|
||||
{
|
||||
[]byte(fstabMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
|
||||
File: "/",
|
||||
VfsType: "ext4",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 1,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
[]byte(fstabMock1),
|
||||
&fstab.Mount{
|
||||
Spec: "/dev/mapper/root",
|
||||
File: "/home",
|
||||
VfsType: "ext4",
|
||||
MntOps: map[string]string{"defaults": ""},
|
||||
Freq: 1,
|
||||
PassNo: 1,
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestFstabEntryExists(t *testing.T) {
|
||||
file, err := ioutil.TempFile("", "fstab")
|
||||
if err != nil {
|
||||
t.Errorf("error creating temp file: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
for _, test := range fstabEntryExistsTests {
|
||||
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||
return
|
||||
}
|
||||
result, err := fstabEntryExists(file.Name(), test.in)
|
||||
if err != nil {
|
||||
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
|
||||
return
|
||||
}
|
||||
if result != test.out {
|
||||
t.Errorf("fstabEntryExists test wanted: %t, got: %t", test.out, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountCompare(t *testing.T) {
|
||||
for _, test := range mountCompareTests {
|
||||
result, err := mountCompare(test.dIn, test.pIn)
|
||||
if err != nil {
|
||||
t.Errorf("error comparing mounts: %s and %s: %v", test.dIn.String(), test.pIn.String(), err)
|
||||
return
|
||||
}
|
||||
if result != test.out {
|
||||
t.Errorf("mountCompare test wanted: %t, got: %t", test.out, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,20 +19,26 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("msg", func() Res { return &MsgRes{} })
|
||||
engine.RegisterResource("msg", func() engine.Res { return &MsgRes{} })
|
||||
}
|
||||
|
||||
// MsgRes is a resource that writes messages to logs.
|
||||
type MsgRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Body string `yaml:"body"`
|
||||
Priority string `yaml:"priority"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
@@ -43,19 +49,9 @@ type MsgRes struct {
|
||||
syslogStateOK bool
|
||||
}
|
||||
|
||||
// MsgUID is a unique representation for a MsgRes object.
|
||||
type MsgUID struct {
|
||||
BaseUID
|
||||
body string
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *MsgRes) Default() Res {
|
||||
return &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *MsgRes) Default() engine.Res {
|
||||
return &MsgRes{}
|
||||
}
|
||||
|
||||
// Validate the params that are passed to MsgRes.
|
||||
@@ -81,15 +77,42 @@ func (obj *MsgRes) Validate() error {
|
||||
default:
|
||||
return fmt.Errorf("invalid Priority '%s'", obj.Priority)
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *MsgRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overrriding
|
||||
func (obj *MsgRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isAllStateOK derives a compound state from all internal cache flags that apply to this resource.
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *MsgRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *MsgRes) Watch() error {
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
//var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
//if send {
|
||||
// send = false
|
||||
// obj.init.Event() // notify engine of an event (this can block)
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
// isAllStateOK derives a compound state from all internal cache flags that
|
||||
// apply to this resource.
|
||||
func (obj *MsgRes) isAllStateOK() bool {
|
||||
if obj.Journal && !obj.journalStateOK {
|
||||
return false
|
||||
@@ -102,7 +125,10 @@ func (obj *MsgRes) isAllStateOK() bool {
|
||||
|
||||
// updateStateOK sets the global state so it can be read by the engine.
|
||||
func (obj *MsgRes) updateStateOK() {
|
||||
obj.StateOK(obj.isAllStateOK())
|
||||
// XXX: this resource doesn't entirely make sense to me at the moment.
|
||||
if !obj.isAllStateOK() {
|
||||
//obj.init.Dirty() // XXX: removed with API cleanup
|
||||
}
|
||||
}
|
||||
|
||||
// JournalPriority converts a string description to a numeric priority.
|
||||
@@ -128,42 +154,15 @@ func (obj *MsgRes) journalPriority() journal.Priority {
|
||||
return journal.PriNotice
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *MsgRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Msg resource.
|
||||
// Every check leads to an apply, meaning that the message is flushed to the journal.
|
||||
// CheckApply method for Msg resource. Every check leads to an apply, meaning
|
||||
// that the message is flushed to the journal.
|
||||
func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||
|
||||
// isStateOK() done by engine, so we updateStateOK() to pass in value
|
||||
//if obj.isAllStateOK() {
|
||||
// return true, nil
|
||||
//}
|
||||
|
||||
if obj.Refresh() { // if we were notified...
|
||||
if obj.init.Refresh() { // if we were notified...
|
||||
// invalidate cached state...
|
||||
obj.logStateOK = false
|
||||
if obj.Journal {
|
||||
@@ -176,7 +175,7 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||
}
|
||||
|
||||
if !obj.logStateOK {
|
||||
log.Printf("%s: Body: %s", obj, obj.Body)
|
||||
obj.init.Logf("Body: %s", obj.Body)
|
||||
obj.logStateOK = true
|
||||
obj.updateStateOK()
|
||||
}
|
||||
@@ -199,50 +198,51 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *MsgRes) UIDs() []ResUID {
|
||||
x := &MsgUID{
|
||||
BaseUID: BaseUID{
|
||||
Name: obj.GetName(),
|
||||
Kind: obj.GetKind(),
|
||||
},
|
||||
body: obj.Body,
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *MsgRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *MsgRes) Cmp(r engine.Res) error {
|
||||
// we can only compare MsgRes to others of the same resource kind
|
||||
res, ok := r.(*MsgRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) {
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Body != res.Body {
|
||||
return false
|
||||
return fmt.Errorf("the Body differs")
|
||||
}
|
||||
if obj.Priority != res.Priority {
|
||||
return false
|
||||
return fmt.Errorf("the Priority differs")
|
||||
}
|
||||
if len(obj.Fields) != len(res.Fields) {
|
||||
return false
|
||||
return fmt.Errorf("the length of Fields differs")
|
||||
}
|
||||
for field, value := range obj.Fields {
|
||||
if res.Fields[field] != value {
|
||||
return false
|
||||
return fmt.Errorf("the Fields differ")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// MsgUID is a unique representation for a MsgRes object.
|
||||
type MsgUID struct {
|
||||
engine.BaseUID
|
||||
|
||||
body string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *MsgRes) UIDs() []engine.ResUID {
|
||||
x := &MsgUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
body: obj.Body,
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes MsgRes // indirection to avoid infinite recursion
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,6 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
@@ -23,15 +25,9 @@ import (
|
||||
|
||||
func TestMsgValidate1(t *testing.T) {
|
||||
r1 := &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "msg1",
|
||||
Kind: "msg",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Priority: "Debug",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
@@ -39,15 +35,9 @@ func TestMsgValidate1(t *testing.T) {
|
||||
|
||||
func TestMsgValidate2(t *testing.T) {
|
||||
r1 := &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "msg1",
|
||||
Kind: "msg",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Priority: "UnrealPriority",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err == nil {
|
||||
t.Errorf("validation error is nil")
|
||||
}
|
||||
773
engine/resources/net.go
Normal file
773
engine/resources/net.go
Normal file
@@ -0,0 +1,773 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !darwin
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
"github.com/purpleidea/mgmt/util/socketset"
|
||||
|
||||
// XXX: Do NOT use subscribe methods from this lib, as they are racey and
|
||||
// do not clean up spawned goroutines. Should be replaced when a suitable
|
||||
// alternative is available.
|
||||
"github.com/vishvananda/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("net", func() engine.Res { return &NetRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
// IfacePrefix is the prefix used to identify unit files for managed links.
|
||||
IfacePrefix = "mgmt-"
|
||||
// networkdUnitFileDir is the location of networkd unit files which define
|
||||
// the systemd network connections.
|
||||
networkdUnitFileDir = "/etc/systemd/network/"
|
||||
// networkdUnitFileExt is the file extension for networkd unit files.
|
||||
networkdUnitFileExt = ".network"
|
||||
// networkdUnitFileUmask sets the permissions on the systemd unit file.
|
||||
networkdUnitFileUmask = 0644
|
||||
|
||||
// ifaceUp is the up (on) interface state.
|
||||
ifaceUp = "up"
|
||||
// ifaceDown is the down (off) interface state.
|
||||
ifaceDown = "down"
|
||||
|
||||
// Netlink multicast groups to watch for events. For all groups see:
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
||||
rtmGrps = rtmGrpLink | rtmGrpIPv4IfAddr | rtmGrpIPv6IfAddr | rtmGrpIPv4IfRoute
|
||||
rtmGrpLink = 0x1 // interface create/delete/up/down
|
||||
rtmGrpIPv4IfAddr = 0x10 // add/delete IPv4 addresses
|
||||
rtmGrpIPv6IfAddr = 0x100 // add/delete IPv6 addresses
|
||||
rtmGrpIPv4IfRoute = 0x40 // add delete routes
|
||||
|
||||
// IP routing protocols for used for netlink route messages. For all
|
||||
// protocols see:
|
||||
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
||||
rtProtoKernel = 2 // kernel
|
||||
rtProtoStatic = 4 // static
|
||||
|
||||
socketFile = "pipe.sock" // path in vardir to store our socket file
|
||||
)
|
||||
|
||||
// NetRes is a network interface resource based on netlink. It manages the state
|
||||
// of a network link. Configuration is also stored in a networkd configuration
|
||||
// file, so the network is available upon reboot. The name of the resource is
|
||||
// the string representing the network interface name. This could be "eth0" for
|
||||
// example.
|
||||
type NetRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// State is the desired state of the interface. It can be "up", "down",
|
||||
// or the empty string to leave that unspecified.
|
||||
State string `lang:"state" yaml:"state"`
|
||||
|
||||
// Addrs is the list of addresses to set on the interface. They must
|
||||
// each be in CIDR notation such as: 192.0.2.42/24 for example.
|
||||
Addrs []string `lang:"addrs" yaml:"addrs"`
|
||||
|
||||
// Gateway represents the default route to set for the interface.
|
||||
Gateway string `lang:"gateway" yaml:"gateway"`
|
||||
|
||||
// IPForward is a boolean that sets whether we should forward incoming
|
||||
// packets onward when this is set. It default to unspecified, which
|
||||
// downstream (in the systemd-networkd configuration) defaults to false.
|
||||
// XXX: this could also be "ipv4" or "ipv6", add those as a second option?
|
||||
IPForward *bool `lang:"ip_forward" yaml:"ip_forward"`
|
||||
|
||||
iface *iface // a struct containing the net.Interface and netlink.Link
|
||||
unitFilePath string // the interface unit file path
|
||||
|
||||
socketFile string // path for storing the pipe socket file
|
||||
}
|
||||
|
||||
// nlChanStruct defines the channel used to send netlink messages and errors to
|
||||
// the event processing loop in Watch.
|
||||
type nlChanStruct struct {
|
||||
msg []syscall.NetlinkMessage
|
||||
err error
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *NetRes) Default() engine.Res {
|
||||
return &NetRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *NetRes) Validate() error {
|
||||
// validate state
|
||||
if obj.State != ifaceUp && obj.State != ifaceDown && obj.State != "" {
|
||||
return fmt.Errorf("state must be up, down or empty")
|
||||
}
|
||||
|
||||
// validate network address input
|
||||
if obj.Addrs != nil {
|
||||
for _, addr := range obj.Addrs {
|
||||
if _, _, err := net.ParseCIDR(addr); err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing address: %s", addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
if obj.Gateway != "" {
|
||||
if g := net.ParseIP(obj.Gateway); g == nil {
|
||||
return fmt.Errorf("error parsing gateway: %s", obj.Gateway)
|
||||
}
|
||||
}
|
||||
|
||||
// validate the interface name
|
||||
_, err := net.InterfaceByName(obj.Name())
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *NetRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
var err error
|
||||
|
||||
// tmp directory for pipe socket
|
||||
dir, err := obj.init.VarDir("")
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||
}
|
||||
obj.socketFile = path.Join(dir, socketFile) // return a unique file
|
||||
|
||||
// store the network interface in the struct
|
||||
obj.iface = &iface{}
|
||||
if obj.iface.iface, err = net.InterfaceByName(obj.Name()); err != nil {
|
||||
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
||||
}
|
||||
// store the netlink link to use as interface input in netlink functions
|
||||
if obj.iface.link, err = netlink.LinkByName(obj.Name()); err != nil {
|
||||
return errwrap.Wrapf(err, "error finding link: %s", obj.Name())
|
||||
}
|
||||
|
||||
// build the path to the networkd configuration file
|
||||
obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.Name() + networkdUnitFileExt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close cleans up when we're done.
|
||||
func (obj *NetRes) Close() error {
|
||||
var errList error
|
||||
|
||||
if obj.socketFile == "/" {
|
||||
return fmt.Errorf("socket file should not be the root path")
|
||||
}
|
||||
if obj.socketFile != "" { // safety
|
||||
errList = errwrap.Append(errList, os.Remove(obj.socketFile))
|
||||
}
|
||||
|
||||
return errList
|
||||
}
|
||||
|
||||
// Watch listens for events from the specified interface via a netlink socket.
|
||||
// TODO: currently gets events from ALL interfaces, would be nice to reject
|
||||
// events from other interfaces.
|
||||
func (obj *NetRes) Watch() error {
|
||||
// create a netlink socket for receiving network interface events
|
||||
conn, err := socketset.NewSocketSet(rtmGrps, obj.socketFile, unix.NETLINK_ROUTE)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error creating socket set")
|
||||
}
|
||||
|
||||
// waitgroup for netlink receive goroutine
|
||||
wg := &sync.WaitGroup{}
|
||||
defer conn.Close()
|
||||
// We must wait for the Shutdown() AND the select inside of SocketSet to
|
||||
// complete before we Close, since the unblocking in SocketSet is not a
|
||||
// synchronous operation.
|
||||
defer wg.Wait()
|
||||
defer conn.Shutdown() // close the netlink socket and unblock conn.receive()
|
||||
|
||||
// watch the systemd-networkd configuration file
|
||||
recWatcher, err := recwatch.NewRecWatcher(obj.unitFilePath, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// close the recwatcher when we're done
|
||||
defer recWatcher.Close()
|
||||
|
||||
// channel for netlink messages
|
||||
nlChan := make(chan *nlChanStruct) // closed from goroutine
|
||||
|
||||
// channel to unblock selects in goroutine
|
||||
closeChan := make(chan struct{})
|
||||
defer close(closeChan)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(nlChan)
|
||||
for {
|
||||
// receive messages from the socket set
|
||||
msgs, err := conn.ReceiveNetlinkMessages()
|
||||
if err != nil {
|
||||
select {
|
||||
case nlChan <- &nlChanStruct{
|
||||
err: errwrap.Wrapf(err, "error receiving messages"),
|
||||
}:
|
||||
case <-closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
select {
|
||||
case nlChan <- &nlChanStruct{
|
||||
msg: msgs,
|
||||
}:
|
||||
case <-closeChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var done bool
|
||||
for {
|
||||
select {
|
||||
case s, ok := <-nlChan:
|
||||
if !ok {
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
done = true
|
||||
continue
|
||||
}
|
||||
if err := s.err; err != nil {
|
||||
return errwrap.Wrapf(s.err, "unknown netlink error")
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event: %+v", s.msg)
|
||||
}
|
||||
|
||||
send = true
|
||||
|
||||
case event, ok := <-recWatcher.Events():
|
||||
if !ok {
|
||||
if done {
|
||||
return nil
|
||||
}
|
||||
done = true
|
||||
continue
|
||||
}
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "unknown recwatcher error")
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
|
||||
send = true
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ifaceCheckApply checks the state of the network device and brings it up or
|
||||
// down as necessary.
|
||||
func (obj *NetRes) ifaceCheckApply(apply bool) (bool, error) {
|
||||
// check the interface state
|
||||
state, err := obj.iface.state()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking %s state", obj.Name())
|
||||
}
|
||||
// if the state is correct or unspecified, we're done
|
||||
if obj.State == state || obj.State == "" {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// end of state checking
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("ifaceCheckApply(%t)", apply)
|
||||
|
||||
// ip link set up/down
|
||||
if err := obj.iface.linkUpDown(obj.State); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error setting %s up or down", obj.Name())
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// addrCheckApply checks if the interface has the correct addresses and then
|
||||
// adds/deletes addresses as necessary.
|
||||
func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
|
||||
// get the link's addresses
|
||||
ifaceAddrs, err := obj.iface.getAddrs()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.Name())
|
||||
}
|
||||
// if state is not defined
|
||||
if obj.Addrs == nil {
|
||||
// send addrs
|
||||
obj.Addrs = ifaceAddrs
|
||||
return true, nil
|
||||
}
|
||||
// check if all addrs have a kernel route needed for first hop
|
||||
kernelOK, err := obj.iface.kernelCheck(obj.Addrs)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking kernel routes")
|
||||
}
|
||||
|
||||
// if the kernel routes are intact and the addrs match, we're done
|
||||
err = util.SortedStrSliceCompare(obj.Addrs, ifaceAddrs)
|
||||
if err == nil && kernelOK {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// end of state checking
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("addrCheckApply(%t)", apply)
|
||||
|
||||
// check each address and delete the ones that aren't in the definition
|
||||
if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking or deleting addresses")
|
||||
}
|
||||
// check each address and add the ones that are defined but do not exist
|
||||
if err := obj.iface.addrApplyAdd(obj.Addrs); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error checking or adding addresses")
|
||||
}
|
||||
// make sure all the addrs have the appropriate kernel routes
|
||||
if err := obj.iface.kernelApply(obj.Addrs); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error adding kernel routes")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// gatewayCheckApply checks if the interface has the correct default gateway and
|
||||
// adds/deletes routes as necessary.
|
||||
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
|
||||
// get all routes from the interface
|
||||
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error getting default routes")
|
||||
}
|
||||
// add default routes to a slice
|
||||
defRoutes := []netlink.Route{}
|
||||
for _, route := range routes {
|
||||
if route.Dst == nil { // route is default
|
||||
defRoutes = append(defRoutes, route)
|
||||
}
|
||||
}
|
||||
// if the gateway is already set, we're done
|
||||
if len(defRoutes) == 1 && defRoutes[0].Gw.String() == obj.Gateway {
|
||||
return true, nil
|
||||
}
|
||||
// if no gateway was defined
|
||||
if obj.Gateway == "" {
|
||||
// send the gateway if there is one
|
||||
if len(defRoutes) == 1 {
|
||||
obj.Gateway = defRoutes[0].Gw.String()
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// end of state checking
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("gatewayCheckApply(%t)", apply)
|
||||
|
||||
// delete all but one default route
|
||||
for i := 1; i < len(defRoutes); i++ {
|
||||
if err := netlink.RouteDel(&defRoutes[i]); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error deleting route: %+v", defRoutes[i])
|
||||
}
|
||||
}
|
||||
|
||||
// add or change the default route
|
||||
if err := netlink.RouteReplace(&netlink.Route{
|
||||
LinkIndex: obj.iface.iface.Index,
|
||||
Gw: net.ParseIP(obj.Gateway),
|
||||
Protocol: rtProtoStatic,
|
||||
}); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error replacing default route")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// fileCheckApply checks and maintains the systemd-networkd unit file contents.
|
||||
func (obj *NetRes) fileCheckApply(apply bool) (bool, error) {
|
||||
// check if the unit file exists
|
||||
_, err := os.Stat(obj.unitFilePath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, errwrap.Wrapf(err, "error checking file")
|
||||
}
|
||||
// build the unit file contents from the definition
|
||||
contents := obj.unitFileContents()
|
||||
// check the file contents
|
||||
if err == nil {
|
||||
unitFile, err := ioutil.ReadFile(obj.unitFilePath)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error reading file")
|
||||
}
|
||||
// return if the file is good
|
||||
if bytes.Equal(unitFile, contents) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
obj.init.Logf("fileCheckApply(%t)", apply)
|
||||
|
||||
// write the file
|
||||
if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error writing configuration file")
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *NetRes) CheckApply(apply bool) (bool, error) {
|
||||
checkOK := true
|
||||
|
||||
// check the network device
|
||||
if c, err := obj.ifaceCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
// if the interface is supposed to be down, we're done
|
||||
if obj.State == ifaceDown {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// check the addresses
|
||||
if c, err := obj.addrCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
// check the gateway
|
||||
if c, err := obj.gatewayCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
// if the state is unspecified, we're done
|
||||
if obj.State == "" {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// check the networkd unit file
|
||||
if c, err := obj.fileCheckApply(apply); err != nil {
|
||||
return false, err
|
||||
} else if !c {
|
||||
checkOK = false
|
||||
}
|
||||
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *NetRes) Cmp(r engine.Res) error {
|
||||
// we can only compare NetRes to others of the same resource kind
|
||||
res, ok := r.(*NetRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
if (obj.Addrs == nil) != (res.Addrs == nil) {
|
||||
return fmt.Errorf("the Addrs differ")
|
||||
}
|
||||
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
|
||||
return fmt.Errorf("the Addrs differ")
|
||||
}
|
||||
if obj.Gateway != res.Gateway {
|
||||
return fmt.Errorf("the Gateway differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetUID is a unique resource identifier.
|
||||
type NetUID struct {
|
||||
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||
// information about where this UID came from, and is unrelated to the
|
||||
// information about the resource we're matching. That data which is
|
||||
// used in the IFF function, is what you see in the struct fields here.
|
||||
engine.BaseUID
|
||||
|
||||
name string // the network interface name
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *NetUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*NetUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.name == res.name
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one although some resources can return multiple.
|
||||
func (obj *NetRes) UIDs() []engine.ResUID {
|
||||
x := &NetUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes NetRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*NetRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to NetRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = NetRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// unitFileContents builds the unit file contents from the definition.
|
||||
func (obj *NetRes) unitFileContents() []byte {
|
||||
// build the unit file contents
|
||||
u := []string{"[Match]"}
|
||||
u = append(u, fmt.Sprintf("Name=%s", obj.Name()))
|
||||
u = append(u, "[Network]")
|
||||
for _, addr := range obj.Addrs {
|
||||
u = append(u, fmt.Sprintf("Address=%s", addr))
|
||||
}
|
||||
if obj.Gateway != "" {
|
||||
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
|
||||
}
|
||||
if obj.IPForward != nil {
|
||||
b := "false"
|
||||
if *obj.IPForward {
|
||||
b = "true"
|
||||
}
|
||||
u = append(u, fmt.Sprintf("IPForward=%s", b))
|
||||
}
|
||||
c := strings.Join(u, "\n")
|
||||
return []byte(c)
|
||||
}
|
||||
|
||||
// iface wraps net.Interface to add additional methods.
|
||||
type iface struct {
|
||||
iface *net.Interface
|
||||
link netlink.Link
|
||||
}
|
||||
|
||||
// state reports the state of the interface as up or down.
|
||||
func (obj *iface) state() (string, error) {
|
||||
var err error
|
||||
if obj.iface, err = net.InterfaceByName(obj.iface.Name); err != nil {
|
||||
return "", errwrap.Wrapf(err, "error updating interface")
|
||||
}
|
||||
// if the interface's "up" flag is 0, it's down
|
||||
if obj.iface.Flags&net.FlagUp == 0 {
|
||||
return ifaceDown, nil
|
||||
}
|
||||
// otherwise it's up
|
||||
return ifaceUp, nil
|
||||
}
|
||||
|
||||
// linkUpDown brings the interface up or down, depending on input value.
|
||||
func (obj *iface) linkUpDown(state string) error {
|
||||
if state != ifaceUp && state != ifaceDown {
|
||||
return fmt.Errorf("state must be up or down")
|
||||
}
|
||||
if state == ifaceUp {
|
||||
return netlink.LinkSetUp(obj.link)
|
||||
}
|
||||
return netlink.LinkSetDown(obj.link)
|
||||
}
|
||||
|
||||
// getAddrs returns a list of strings containing all of the interface's IP
|
||||
// addresses in CIDR format.
|
||||
func (obj *iface) getAddrs() ([]string, error) {
|
||||
var ifaceAddrs []string
|
||||
a, err := obj.iface.Addrs()
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||
}
|
||||
// we're only interested in the strings (not the network)
|
||||
for _, addr := range a {
|
||||
ifaceAddrs = append(ifaceAddrs, addr.String())
|
||||
}
|
||||
return ifaceAddrs, nil
|
||||
}
|
||||
|
||||
// kernelCheck checks if all addresses in the list have a corresponding kernel
|
||||
// route, without which the network would be unreachable.
|
||||
func (obj *iface) kernelCheck(addrs []string) (bool, error) {
|
||||
var routeOK bool
|
||||
|
||||
// get a list of all the routes associated with the interface
|
||||
routes, err := netlink.RouteList(obj.link, netlink.FAMILY_V4)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error getting routes")
|
||||
}
|
||||
// check each route against each addr
|
||||
for _, addr := range addrs {
|
||||
routeOK = false
|
||||
ip, ipNet, err := net.ParseCIDR(addr)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error parsing addr: %s", addr)
|
||||
}
|
||||
for _, r := range routes {
|
||||
// if src, dst and protocol are correct, the kernel route exists
|
||||
if r.Src.Equal(ip) && r.Dst.String() == ipNet.String() && r.Protocol == rtProtoKernel {
|
||||
routeOK = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// if any addr is missing a kernel route return early
|
||||
if !routeOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
return routeOK, nil
|
||||
}
|
||||
|
||||
// kernelApply adds or replaces each address' kernel route as necessary.
|
||||
func (obj *iface) kernelApply(addrs []string) error {
|
||||
// for each addr, add or replace the corresponding kernel route
|
||||
for _, addr := range addrs {
|
||||
ip, ipNet, err := net.ParseCIDR(addr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing addr: %s", addr)
|
||||
}
|
||||
// kernel route needed for the network to be reachable from a given ip
|
||||
if err := netlink.RouteReplace(&netlink.Route{
|
||||
LinkIndex: obj.iface.Index,
|
||||
Dst: ipNet,
|
||||
Src: ip,
|
||||
Protocol: rtProtoKernel,
|
||||
Scope: netlink.SCOPE_LINK,
|
||||
}); err != nil {
|
||||
return errwrap.Wrapf(err, "error replacing first hop route")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addrApplyDelete, checks the interface's addresses and deletes any that are
|
||||
// not in the list/definition.
|
||||
func (obj *iface) addrApplyDelete(objAddrs []string) error {
|
||||
ifaceAddrs, err := obj.getAddrs()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||
}
|
||||
for _, ifaceAddr := range ifaceAddrs {
|
||||
addrOK := false
|
||||
for _, objAddr := range objAddrs {
|
||||
if ifaceAddr == objAddr {
|
||||
addrOK = true
|
||||
}
|
||||
}
|
||||
if addrOK {
|
||||
continue
|
||||
}
|
||||
addr, err := netlink.ParseAddr(ifaceAddr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing netlink address: %s", ifaceAddr)
|
||||
}
|
||||
if err := netlink.AddrDel(obj.link, addr); err != nil {
|
||||
return errwrap.Wrapf(err, "error deleting addr: %s from %s", ifaceAddr, obj.iface.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addrApplyAdd checks if the interface has each address in the supplied list,
|
||||
// and if it doesn't, it adds them.
|
||||
func (obj *iface) addrApplyAdd(objAddrs []string) error {
|
||||
ifaceAddrs, err := obj.getAddrs()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||
}
|
||||
for _, objAddr := range objAddrs {
|
||||
addrOK := false
|
||||
for _, ifaceAddr := range ifaceAddrs {
|
||||
if ifaceAddr == objAddr {
|
||||
addrOK = true
|
||||
}
|
||||
}
|
||||
if addrOK {
|
||||
continue
|
||||
}
|
||||
addr, err := netlink.ParseAddr(objAddr)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error parsing cidr address: %s", objAddr)
|
||||
}
|
||||
if err := netlink.AddrAdd(obj.link, addr); err != nil {
|
||||
return errwrap.Wrapf(err, "error adding addr: %s to %s", objAddr, obj.iface.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
engine/resources/net_test.go
Normal file
84
engine/resources/net_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !darwin
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// test cases for NetRes.unitFileContents()
|
||||
var unitFileContentsTests = []struct {
|
||||
dev string
|
||||
in *NetRes
|
||||
out []byte
|
||||
}{
|
||||
{
|
||||
"eth0",
|
||||
&NetRes{
|
||||
State: "up",
|
||||
Addrs: []string{"192.168.42.13/24"},
|
||||
Gateway: "192.168.42.1",
|
||||
},
|
||||
[]byte(
|
||||
strings.Join(
|
||||
[]string{
|
||||
"[Match]",
|
||||
"Name=eth0",
|
||||
"[Network]",
|
||||
"Address=192.168.42.13/24",
|
||||
"Gateway=192.168.42.1",
|
||||
},
|
||||
"\n"),
|
||||
),
|
||||
},
|
||||
{
|
||||
"wlp5s0",
|
||||
&NetRes{
|
||||
State: "up",
|
||||
Addrs: []string{"10.0.2.13/24", "10.0.2.42/24"},
|
||||
Gateway: "10.0.2.1",
|
||||
},
|
||||
[]byte(
|
||||
strings.Join(
|
||||
[]string{
|
||||
"[Match]",
|
||||
"Name=wlp5s0",
|
||||
"[Network]",
|
||||
"Address=10.0.2.13/24",
|
||||
"Address=10.0.2.42/24",
|
||||
"Gateway=10.0.2.1",
|
||||
},
|
||||
"\n"),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
// test NetRes.unitFileContents()
|
||||
func TestUnitFileContents(t *testing.T) {
|
||||
for _, test := range unitFileContentsTests {
|
||||
test.in.SetName(test.dev)
|
||||
result := test.in.unitFileContents()
|
||||
if !bytes.Equal(test.out, result) {
|
||||
t.Errorf("nfd test wanted:\n %s, got:\n %s", test.out, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,121 +19,115 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("noop", func() Res { return &NoopRes{} })
|
||||
engine.RegisterResource("noop", func() engine.Res { return &NoopRes{} })
|
||||
}
|
||||
|
||||
// NoopRes is a no-op resource that does nothing.
|
||||
type NoopRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Groupable
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Comment string `lang:"comment" yaml:"comment"` // extra field for example purposes
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *NoopRes) Default() Res {
|
||||
return &NoopRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *NoopRes) Default() engine.Res {
|
||||
return &NoopRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *NoopRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *NoopRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *NoopRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *NoopRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *NoopRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
select {
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
}
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckApply method for Noop resource. Does nothing, returns happy!
|
||||
func (obj *NoopRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if obj.Refresh() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
func (obj *NoopRes) CheckApply(apply bool) (bool, error) {
|
||||
if obj.init.Refresh() {
|
||||
obj.init.Logf("received a notification!")
|
||||
}
|
||||
return true, nil // state is always okay
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *NoopRes) Cmp(r engine.Res) error {
|
||||
// we can only compare NoopRes to others of the same resource kind
|
||||
res, ok := r.(*NoopRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Comment != res.Comment {
|
||||
return fmt.Errorf("the Comment differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NoopUID is the UID struct for NoopRes.
|
||||
type NoopUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *NoopRes) UIDs() []ResUID {
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *NoopRes) UIDs() []engine.ResUID {
|
||||
x := &NoopUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []ResUID{x}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *NoopRes) GroupCmp(r Res) bool {
|
||||
func (obj *NoopRes) GroupCmp(r engine.GroupableRes) error {
|
||||
_, ok := r.(*NoopRes)
|
||||
if !ok {
|
||||
// NOTE: technically we could group a noop into any other
|
||||
// resource, if that resource knew how to handle it, although,
|
||||
// since the mechanics of inter-kind resource grouping are
|
||||
// tricky, avoid doing this until there's a good reason.
|
||||
return false
|
||||
return fmt.Errorf("resource is not the same kind")
|
||||
}
|
||||
return true // noop resources can always be grouped together!
|
||||
return nil // noop resources can always be grouped together!
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *NoopRes) Compare(r Res) bool {
|
||||
// we can only compare NoopRes to others of the same resource kind
|
||||
res, ok := r.(*NoopRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// calling base Compare is probably unneeded for the noop res, but do it
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *NoopRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes NoopRes // indirection to avoid infinite recursion
|
||||
|
||||
104
engine/resources/noop_test.go
Normal file
104
engine/resources/noop_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
)
|
||||
|
||||
func TestCmp1(t *testing.T) {
|
||||
r1, err := engine.NewResource("noop")
|
||||
if err != nil {
|
||||
t.Errorf("could not create resource: %+v", err)
|
||||
}
|
||||
r2, err := engine.NewResource("noop")
|
||||
if err != nil {
|
||||
t.Errorf("could not create resource: %+v", err)
|
||||
}
|
||||
r3, err := engine.NewResource("file")
|
||||
if err != nil {
|
||||
t.Errorf("could not create resource: %+v", err)
|
||||
}
|
||||
|
||||
if err := r1.Cmp(r2); err != nil {
|
||||
t.Errorf("the two resources do not match: %+v", err)
|
||||
}
|
||||
if err := r2.Cmp(r1); err != nil {
|
||||
t.Errorf("the two resources do not match: %+v", err)
|
||||
}
|
||||
|
||||
if r1.Cmp(r3) == nil {
|
||||
t.Errorf("the two resources should not match")
|
||||
}
|
||||
if r3.Cmp(r1) == nil {
|
||||
t.Errorf("the two resources should not match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSort0(t *testing.T) {
|
||||
rs := []engine.Res{}
|
||||
s := engine.Sort(rs)
|
||||
|
||||
if !reflect.DeepEqual(s, []engine.Res{}) {
|
||||
t.Errorf("sort failed!")
|
||||
if s == nil {
|
||||
t.Logf("output is nil!")
|
||||
} else {
|
||||
str := "Got:"
|
||||
for _, r := range s {
|
||||
str += " " + r.String()
|
||||
}
|
||||
t.Errorf(str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSort1(t *testing.T) {
|
||||
r1, _ := engine.NewNamedResource("noop", "noop1")
|
||||
r2, _ := engine.NewNamedResource("noop", "noop2")
|
||||
r3, _ := engine.NewNamedResource("noop", "noop3")
|
||||
r4, _ := engine.NewNamedResource("noop", "noop4")
|
||||
r5, _ := engine.NewNamedResource("noop", "noop5")
|
||||
r6, _ := engine.NewNamedResource("noop", "noop6")
|
||||
|
||||
rs := []engine.Res{r3, r2, r6, r1, r5, r4}
|
||||
s := engine.Sort(rs)
|
||||
|
||||
if !reflect.DeepEqual(s, []engine.Res{r1, r2, r3, r4, r5, r6}) {
|
||||
t.Errorf("sort failed!")
|
||||
str := "Got:"
|
||||
for _, r := range s {
|
||||
str += " " + r.String()
|
||||
}
|
||||
t.Errorf(str)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(rs, []engine.Res{r3, r2, r6, r1, r5, r4}) {
|
||||
t.Errorf("sort modified input!")
|
||||
str := "Got:"
|
||||
for _, r := range rs {
|
||||
str += " " + r.String()
|
||||
}
|
||||
t.Errorf(str)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,36 +20,44 @@ package resources
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
systemdDbus "github.com/coreos/go-systemd/dbus"
|
||||
machined "github.com/coreos/go-systemd/machine1"
|
||||
systemdUtil "github.com/coreos/go-systemd/util"
|
||||
"github.com/godbus/dbus"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
running = "running"
|
||||
stopped = "stopped"
|
||||
dbusInterface = "org.freedesktop.machine1.Manager"
|
||||
machineNew = "org.freedesktop.machine1.Manager.MachineNew"
|
||||
machineRemoved = "org.freedesktop.machine1.Manager.MachineRemoved"
|
||||
dbusMachine1Iface = "org.freedesktop.machine1.Manager"
|
||||
machineNew = dbusMachine1Iface + ".MachineNew"
|
||||
machineRemoved = dbusMachine1Iface + ".MachineRemoved"
|
||||
nspawnServiceTmpl = "systemd-nspawn@%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("nspawn", func() Res { return &NspawnRes{} })
|
||||
engine.RegisterResource("nspawn", func() engine.Res { return &NspawnRes{} })
|
||||
}
|
||||
|
||||
// NspawnRes is an nspawn container resource.
|
||||
type NspawnRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
//traits.Groupable // TODO: this would be quite useful for this resource
|
||||
traits.Refreshable // needed because we embed a svc res
|
||||
|
||||
init *engine.Init
|
||||
|
||||
State string `yaml:"state"`
|
||||
// We're using the svc resource to start and stop the machine because
|
||||
// that's what machinectl does. We're not using svc.Watch because then we
|
||||
// would have two watches potentially racing each other and producing
|
||||
@@ -59,19 +67,16 @@ type NspawnRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *NspawnRes) Default() Res {
|
||||
func (obj *NspawnRes) Default() engine.Res {
|
||||
return &NspawnRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
State: running,
|
||||
}
|
||||
}
|
||||
|
||||
// makeComposite creates a pointer to a SvcRes. The pointer is used to
|
||||
// validate and initialize the nested svc.
|
||||
// makeComposite creates a pointer to a SvcRes. The pointer is used to validate
|
||||
// and initialize the nested svc.
|
||||
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
||||
res, err := NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.GetName()))
|
||||
res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,6 +85,252 @@ func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *NspawnRes) Validate() error {
|
||||
if len(obj.Name()) > 64 {
|
||||
return fmt.Errorf("name must be 64 characters or less")
|
||||
}
|
||||
// check if systemd version is higher than 231 to allow non-alphanumeric
|
||||
// machine names, as previous versions would error in such cases
|
||||
ver, err := systemdVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ver < 231 {
|
||||
for _, char := range obj.Name() {
|
||||
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
|
||||
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.State != running && obj.State != stopped {
|
||||
return fmt.Errorf("invalid state: %s", obj.State)
|
||||
}
|
||||
|
||||
svc, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||
}
|
||||
if err := svc.Validate(); err != nil { // composite resource
|
||||
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", svc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *NspawnRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
svc, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in init")
|
||||
}
|
||||
obj.svc = svc
|
||||
// TODO: we could build a new init that adds a prefix to the logger...
|
||||
return obj.svc.Init(init)
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *NspawnRes) Close() error {
|
||||
if obj.svc != nil {
|
||||
return obj.svc.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch for state changes and sends a message to the bus if there is a change.
|
||||
func (obj *NspawnRes) Watch() error {
|
||||
// this resource depends on systemd to ensure that it's running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return fmt.Errorf("systemd is not running")
|
||||
}
|
||||
|
||||
// create a private message bus
|
||||
bus, err := util.SystemBusPrivateUsable()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "failed to connect to bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
// add a match rule to match messages going through the message bus
|
||||
args := fmt.Sprintf("type='signal',interface='%s',eavesdrop='true'", dbusMachine1Iface)
|
||||
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
return err
|
||||
}
|
||||
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||
|
||||
busChan := make(chan *dbus.Signal)
|
||||
defer close(busChan)
|
||||
bus.Signal(busChan)
|
||||
defer bus.RemoveSignal(busChan) // not needed here, but nice for symmetry
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case event := <-busChan:
|
||||
// process org.freedesktop.machine1 events for this resource's name
|
||||
if event.Body[0] == obj.Name() {
|
||||
obj.init.Logf("Event received: %v", event.Name)
|
||||
if event.Name == machineNew {
|
||||
obj.init.Logf("Machine started")
|
||||
} else if event.Name == machineRemoved {
|
||||
obj.init.Logf("Machine stopped")
|
||||
} else {
|
||||
return fmt.Errorf("unknown event: %s", event.Name)
|
||||
}
|
||||
send = true
|
||||
}
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *NspawnRes) CheckApply(apply bool) (bool, error) {
|
||||
// this resource depends on systemd to ensure that it's running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return false, errors.New("systemd is not running")
|
||||
}
|
||||
|
||||
// connect to org.freedesktop.machine1.Manager
|
||||
conn, err := machined.New()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to connect to dbus")
|
||||
}
|
||||
|
||||
// compare the current state with the desired state and perform the
|
||||
// appropriate action
|
||||
var exists = true
|
||||
properties, err := conn.DescribeMachine(obj.Name())
|
||||
if err != nil {
|
||||
if err, ok := err.(dbus.Error); ok && err.Name !=
|
||||
"org.freedesktop.machine1.NoSuchMachine" {
|
||||
return false, err
|
||||
}
|
||||
exists = false
|
||||
// if we could not successfully get the properties because
|
||||
// there's no such machine the machine is stopped
|
||||
// error if we need the image ignore if we don't
|
||||
if _, err = conn.GetImage(obj.Name()); err != nil && obj.State != stopped {
|
||||
return false, fmt.Errorf(
|
||||
"no machine nor image named '%s'",
|
||||
obj.Name())
|
||||
}
|
||||
}
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("properties: %v", properties)
|
||||
}
|
||||
// if the machine doesn't exist and is supposed to
|
||||
// be stopped or the state matches we're done
|
||||
if !exists && obj.State == stopped || properties["State"] == obj.State {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("CheckApply() in valid state")
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// end of state checking. if we're here, checkOK is false
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
obj.init.Logf("CheckApply() applying '%s' state", obj.State)
|
||||
// use the embedded svc to apply the correct state
|
||||
if _, err := obj.svc.CheckApply(apply); err != nil {
|
||||
return false, errwrap.Wrapf(err, "nested svc failed")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *NspawnRes) Cmp(r engine.Res) error {
|
||||
// we can only compare NspawnRes to others of the same resource kind
|
||||
res, ok := r.(*NspawnRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("the State differs")
|
||||
}
|
||||
|
||||
// TODO: why is res.svc ever nil?
|
||||
if (obj.svc == nil) != (res.svc == nil) { // xor
|
||||
return fmt.Errorf("the svc differs")
|
||||
}
|
||||
if obj.svc != nil && res.svc != nil {
|
||||
if err := obj.svc.Cmp(res.svc); err != nil {
|
||||
return errwrap.Wrapf(err, "the svc differs")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NspawnUID is a unique resource identifier.
|
||||
type NspawnUID struct {
|
||||
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||
// information about where this UID came from, and is unrelated to the
|
||||
// information about the resource we're matching. That data which is
|
||||
// used in the IFF function, is what you see in the struct fields here.
|
||||
engine.BaseUID
|
||||
|
||||
name string // the machine name
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *NspawnUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*NspawnUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.name == res.name
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one although some resources can return multiple.
|
||||
func (obj *NspawnRes) UIDs() []engine.ResUID {
|
||||
x := &NspawnUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(), // svc name
|
||||
}
|
||||
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes NspawnRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*NspawnRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to NspawnRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = NspawnRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// systemdVersion uses dbus to check which version of systemd is installed.
|
||||
func systemdVersion() (uint16, error) {
|
||||
// check if systemd is running
|
||||
@@ -97,10 +348,12 @@ func systemdVersion() (uint16, error) {
|
||||
return 0, errwrap.Wrapf(err, "could not get version property")
|
||||
}
|
||||
// lose the surrounding quotes
|
||||
verNum, err := strconv.Unquote(verString)
|
||||
verNumString, err := strconv.Unquote(verString)
|
||||
if err != nil {
|
||||
return 0, errwrap.Wrapf(err, "error unquoting version number")
|
||||
}
|
||||
// trim possible version suffix like in "242.19-1"
|
||||
verNum := strings.Split(verNumString, ".")[0]
|
||||
// cast to uint16
|
||||
ver, err := strconv.ParseUint(verNum, 10, 16)
|
||||
if err != nil {
|
||||
@@ -108,266 +361,3 @@ func systemdVersion() (uint16, error) {
|
||||
}
|
||||
return uint16(ver), nil
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *NspawnRes) Validate() error {
|
||||
if len(obj.GetName()) > 64 {
|
||||
return fmt.Errorf("name must be 64 characters or less")
|
||||
}
|
||||
// check if systemd version is higher than 231 to allow non-alphanumeric
|
||||
// machine names, as previous versions would error in such cases
|
||||
ver, err := systemdVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ver < 231 {
|
||||
for _, char := range obj.GetName() {
|
||||
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
|
||||
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if obj.State != running && obj.State != stopped {
|
||||
return fmt.Errorf("invalid state: %s", obj.State)
|
||||
}
|
||||
|
||||
svc, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||
}
|
||||
if err := svc.Validate(); err != nil { // composite resource
|
||||
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *NspawnRes) Init() error {
|
||||
svc, err := obj.makeComposite()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "makeComposite failed in init")
|
||||
}
|
||||
obj.svc = svc
|
||||
if err := obj.svc.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
return obj.BaseRes.Init()
|
||||
}
|
||||
|
||||
// Watch for state changes and sends a message to the bus if there is a change.
|
||||
func (obj *NspawnRes) Watch() error {
|
||||
// this resource depends on systemd to ensure that it's running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return fmt.Errorf("systemd is not running")
|
||||
}
|
||||
|
||||
// create a private message bus
|
||||
bus, err := util.SystemBusPrivateUsable()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "failed to connect to bus")
|
||||
}
|
||||
|
||||
// add a match rule to match messages going through the message bus
|
||||
call := bus.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
|
||||
fmt.Sprintf("type='signal',interface='%s',eavesdrop='true'",
|
||||
dbusInterface))
|
||||
// <-call.Done
|
||||
if err := call.Err; err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: verify that implementation doesn't deadlock if there are unread
|
||||
// messages left in the channel
|
||||
busChan := make(chan *dbus.Signal, 10)
|
||||
bus.Signal(busChan)
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
|
||||
var send = false
|
||||
var exit *error
|
||||
|
||||
defer close(busChan)
|
||||
defer bus.Close()
|
||||
defer bus.RemoveSignal(busChan)
|
||||
for {
|
||||
select {
|
||||
case event := <-busChan:
|
||||
// process org.freedesktop.machine1 events for this resource's name
|
||||
if event.Body[0] == obj.GetName() {
|
||||
log.Printf("%s: Event received: %v", obj, event.Name)
|
||||
if event.Name == machineNew {
|
||||
log.Printf("%s: Machine started", obj)
|
||||
} else if event.Name == machineRemoved {
|
||||
log.Printf("%s: Machine stopped", obj)
|
||||
} else {
|
||||
return fmt.Errorf("unknown event: %s", event.Name)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
}
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||
// necessary changes to reach the desired state. This is run before Watch and
|
||||
// again if Watch finds a change occurring to the state.
|
||||
func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
// this resource depends on systemd to ensure that it's running
|
||||
if !systemdUtil.IsRunningSystemd() {
|
||||
return false, errors.New("systemd is not running")
|
||||
}
|
||||
|
||||
// connect to org.freedesktop.machine1.Manager
|
||||
conn, err := machined.New()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to connect to dbus")
|
||||
}
|
||||
|
||||
// compare the current state with the desired state and perform the
|
||||
// appropriate action
|
||||
var exists = true
|
||||
properties, err := conn.DescribeMachine(obj.GetName())
|
||||
if err != nil {
|
||||
if err, ok := err.(dbus.Error); ok && err.Name !=
|
||||
"org.freedesktop.machine1.NoSuchMachine" {
|
||||
return false, err
|
||||
}
|
||||
exists = false
|
||||
// if we could not successfully get the properties because
|
||||
// there's no such machine the machine is stopped
|
||||
// error if we need the image ignore if we don't
|
||||
if _, err = conn.GetImage(obj.GetName()); err != nil && obj.State != stopped {
|
||||
return false, fmt.Errorf(
|
||||
"no machine nor image named '%s'",
|
||||
obj.GetName())
|
||||
}
|
||||
}
|
||||
if obj.debug {
|
||||
log.Printf("%s: properties: %v", obj, properties)
|
||||
}
|
||||
// if the machine doesn't exist and is supposed to
|
||||
// be stopped or the state matches we're done
|
||||
if !exists && obj.State == stopped || properties["State"] == obj.State {
|
||||
if obj.debug {
|
||||
log.Printf("%s: CheckApply() in valid state", obj)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// end of state checking. if we're here, checkOK is false
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
log.Printf("%s: CheckApply() applying '%s' state", obj, obj.State)
|
||||
// use the embedded svc to apply the correct state
|
||||
if _, err := obj.svc.CheckApply(apply); err != nil {
|
||||
return false, errwrap.Wrapf(err, "nested svc failed")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// NspawnUID is a unique resource identifier.
|
||||
type NspawnUID struct {
|
||||
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||
// information about where this UID came from, and is unrelated to the
|
||||
// information about the resource we're matching. That data which is
|
||||
// used in the IFF function, is what you see in the struct fields here.
|
||||
BaseUID
|
||||
name string // the machine name
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *NspawnUID) IFF(uid ResUID) bool {
|
||||
res, ok := uid.(*NspawnUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.name == res.name
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one although some resources can return multiple.
|
||||
func (obj *NspawnRes) UIDs() []ResUID {
|
||||
x := &NspawnUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name, // svc name
|
||||
}
|
||||
return append([]ResUID{x}, obj.svc.UIDs()...)
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *NspawnRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*NspawnRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO: this would be quite useful for this resource!
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *NspawnRes) Compare(r Res) bool {
|
||||
// we can only compare NspawnRes to others of the same resource kind
|
||||
res, ok := r.(*NspawnRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) {
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
}
|
||||
|
||||
// TODO: why is res.svc ever nil?
|
||||
if (obj.svc == nil) != (res.svc == nil) { // xor
|
||||
return false
|
||||
}
|
||||
if obj.svc != nil && res.svc != nil {
|
||||
if !obj.svc.Compare(res.svc) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes NspawnRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*NspawnRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to NspawnRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = NspawnRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -22,22 +22,23 @@ package packagekit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"github.com/godbus/dbus"
|
||||
)
|
||||
|
||||
// global tweaks of verbosity and code path
|
||||
const (
|
||||
Debug = false
|
||||
Paranoid = false // enable if you see any ghosts
|
||||
)
|
||||
|
||||
// constants which might need to be tweaked or which contain special dbus strings.
|
||||
// constants which might need to be tweaked or which contain special dbus
|
||||
// strings.
|
||||
const (
|
||||
// FIXME: if PkBufferSize is too low, install seems to drop signals
|
||||
PkBufferSize = 1000
|
||||
@@ -47,7 +48,6 @@ const (
|
||||
PkPath = "/org/freedesktop/PackageKit"
|
||||
PkIface = "org.freedesktop.PackageKit"
|
||||
PkIfaceTransaction = PkIface + ".Transaction"
|
||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -57,6 +57,7 @@ var (
|
||||
// TODO: add more values
|
||||
// noarch
|
||||
"noarch": "ANY", // special value "ANY" (noarch as seen in Fedora)
|
||||
"any": "ANY", // special value "ANY" ('any' as seen in ArchLinux)
|
||||
"all": "ANY", // special value "ANY" ('all' as seen in Debian)
|
||||
// fedora
|
||||
"x86_64": "amd64",
|
||||
@@ -71,7 +72,7 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
//type enum_filter uint64
|
||||
// type enum_filter uint64
|
||||
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
|
||||
const ( //static const PkEnumMatch enum_filter[]
|
||||
PkFilterEnumUnknown uint64 = 1 << iota // "unknown"
|
||||
@@ -149,9 +150,13 @@ const ( //typedef enum
|
||||
// Conn is a wrapper struct so we can pass bus connection around in the struct.
|
||||
type Conn struct {
|
||||
conn *dbus.Conn
|
||||
|
||||
Debug bool
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
|
||||
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in
|
||||
// the map values.
|
||||
type PkPackageIDActionData struct {
|
||||
Found bool
|
||||
Installed bool
|
||||
@@ -173,58 +178,75 @@ func NewBus() *Conn {
|
||||
}
|
||||
|
||||
// GetBus gets the dbus connection object.
|
||||
func (bus *Conn) GetBus() *dbus.Conn {
|
||||
return bus.conn
|
||||
func (obj *Conn) GetBus() *dbus.Conn {
|
||||
return obj.conn
|
||||
}
|
||||
|
||||
// Close closes the dbus connection object.
|
||||
func (bus *Conn) Close() error {
|
||||
return bus.conn.Close()
|
||||
func (obj *Conn) Close() error {
|
||||
return obj.conn.Close()
|
||||
}
|
||||
|
||||
// internal helper to add signal matches to the bus, should only be called once
|
||||
func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error {
|
||||
if Debug {
|
||||
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals)
|
||||
// matchSignal is an internal helper to add signal matches to the bus. It should
|
||||
// only be called once.
|
||||
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
|
||||
if obj.Debug {
|
||||
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
|
||||
}
|
||||
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
|
||||
var call *dbus.Call
|
||||
bus := obj.GetBus().BusObject()
|
||||
var argsList []string
|
||||
// cleanup function should be called when done or when AddMatch errors
|
||||
removeSignals := func() error {
|
||||
var errList error
|
||||
for i := len(argsList) - 1; i >= 0; i-- { // last in first out
|
||||
call := bus.Call(engineUtil.DBusRemoveMatch, 0, argsList[i])
|
||||
errList = errwrap.Append(errList, call.Err)
|
||||
}
|
||||
return errList
|
||||
}
|
||||
// TODO: if we make this call many times, we seem to receive signals
|
||||
// that many times... Maybe this should be an object singleton?
|
||||
obj := bus.GetBus().BusObject()
|
||||
var call *dbus.Call
|
||||
pathStr := fmt.Sprintf("%s", path)
|
||||
if len(signals) == 0 {
|
||||
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"'")
|
||||
args := fmt.Sprintf("type='signal', path='%s', interface='%s'", pathStr, iface)
|
||||
argsList = append(argsList, args)
|
||||
call = bus.Call(engineUtil.DBusAddMatch, 0, args)
|
||||
} else {
|
||||
for _, signal := range signals {
|
||||
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'")
|
||||
if call.Err != nil {
|
||||
break
|
||||
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s'", pathStr, iface, signal)
|
||||
argsList = append(argsList, args)
|
||||
if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
break // fail if any one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
defer removeSignals() // ignore the error
|
||||
return nil, call.Err
|
||||
}
|
||||
|
||||
// The caller has to make sure that ch is sufficiently buffered; if a
|
||||
// message arrives when a write to c is not possible, it is discarded!
|
||||
// This can be disastrous if we're waiting for a "Finished" signal!
|
||||
bus.GetBus().Signal(ch)
|
||||
return nil
|
||||
obj.GetBus().Signal(ch)
|
||||
return removeSignals, nil
|
||||
}
|
||||
|
||||
// WatchChanges gets a signal anytime an event happens.
|
||||
func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
func (obj *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
ch := make(chan *dbus.Signal, PkBufferSize)
|
||||
// NOTE: the TransactionListChanged signal fires much more frequently,
|
||||
// but with much less specificity. If we're missing events, report the
|
||||
// issue upstream! The UpdatesChanged signal is what hughsie suggested
|
||||
var signal = "UpdatesChanged"
|
||||
err := bus.matchSignal(ch, PkPath, PkIface, []string{signal})
|
||||
removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if Paranoid { // TODO: this filtering might not be necessary anymore...
|
||||
defer removeSignals() // ignore the error
|
||||
if Paranoid { // TODO: this filtering might not be necessary anymore...
|
||||
// try to handle the filtering inside this function!
|
||||
rch := make(chan *dbus.Signal)
|
||||
go func() {
|
||||
@@ -236,13 +258,13 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
// zero value immediately": if i get nil here,
|
||||
// it means the channel was closed by someone!!
|
||||
if event == nil { // shared bus issue?
|
||||
log.Println("PackageKit: Hrm, channel was closed!")
|
||||
obj.Logf("Hrm, channel was closed!")
|
||||
break loop // TODO: continue?
|
||||
}
|
||||
// i think this was caused by using the shared
|
||||
// bus, but we might as well leave it in for now
|
||||
if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) {
|
||||
log.Printf("PackageKit: Woops: Event: %+v", event)
|
||||
obj.Logf("Woops: Event: %+v", event)
|
||||
continue
|
||||
}
|
||||
rch <- event // forward...
|
||||
@@ -256,41 +278,45 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
}
|
||||
|
||||
// CreateTransaction creates and returns a transaction path.
|
||||
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
||||
if Debug {
|
||||
log.Println("PackageKit: CreateTransaction()")
|
||||
func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
||||
if obj.Debug {
|
||||
obj.Logf("CreateTransaction()")
|
||||
}
|
||||
var interfacePath dbus.ObjectPath
|
||||
obj := bus.GetBus().Object(PkIface, PkPath)
|
||||
call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
||||
bus := obj.GetBus().Object(PkIface, PkPath)
|
||||
call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
||||
if call != nil {
|
||||
return "", call
|
||||
}
|
||||
if Debug {
|
||||
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath)
|
||||
if obj.Debug {
|
||||
obj.Logf("CreateTransaction(): %v", interfacePath)
|
||||
}
|
||||
return interfacePath, nil
|
||||
}
|
||||
|
||||
// ResolvePackages runs the PackageKit Resolve method and returns the result.
|
||||
func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
|
||||
func (obj *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
|
||||
packageIDs := []string{}
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
||||
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
// add signal matches for Package and Finished which will always be last
|
||||
var signals = []string{"Package", "Finished", "Error", "Destroy"}
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if Debug {
|
||||
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
|
||||
if Debug {
|
||||
log.Println("PackageKit: ResolvePackages(): Call: Success!")
|
||||
defer removeSignals()
|
||||
if obj.Debug {
|
||||
obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath)
|
||||
}
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
|
||||
if obj.Debug {
|
||||
obj.Logf("ResolvePackages(): Call: Success!")
|
||||
}
|
||||
if call.Err != nil {
|
||||
return []string{}, call.Err
|
||||
@@ -300,11 +326,11 @@ loop:
|
||||
// FIXME: add a timeout option to error in case signals are dropped!
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if Debug {
|
||||
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal)
|
||||
if obj.Debug {
|
||||
obj.Logf("ResolvePackages(): Signal: %+v", signal)
|
||||
}
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -329,7 +355,7 @@ loop:
|
||||
// should already be broken
|
||||
break loop
|
||||
} else {
|
||||
return []string{}, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return []string{}, fmt.Errorf("error in body: %v", signal.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,12 +363,12 @@ loop:
|
||||
}
|
||||
|
||||
// IsInstalledList queries a list of packages to see if they are installed.
|
||||
func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
||||
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
||||
var filter uint64 // initializes at the "zero" value of 0
|
||||
filter += PkFilterEnumArch // always search in our arch
|
||||
packageIDs, e := bus.ResolvePackages(packages, filter)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("ResolvePackages error: %v", e)
|
||||
packageIDs, err := obj.ResolvePackages(packages, filter)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error resolving packages")
|
||||
}
|
||||
|
||||
var m = make(map[string]int)
|
||||
@@ -375,8 +401,8 @@ func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
||||
|
||||
// IsInstalled returns if a package is installed.
|
||||
// TODO: this could be optimized by making the resolve call directly
|
||||
func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
p, e := bus.IsInstalledList([]string{pkg})
|
||||
func (obj *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
p, e := obj.IsInstalledList([]string{pkg})
|
||||
if len(p) != 1 {
|
||||
return false, e
|
||||
}
|
||||
@@ -384,23 +410,27 @@ func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
}
|
||||
|
||||
// InstallPackages installs a list of packages by packageID.
|
||||
func (bus *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
|
||||
func (obj *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
|
||||
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
||||
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeSignals()
|
||||
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("RefreshCache"), 0, false)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
call = obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
||||
call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -411,12 +441,12 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||
// a package was installed...
|
||||
// only start the timer once we're here...
|
||||
@@ -427,34 +457,38 @@ loop:
|
||||
} else if signal.Name == FmtTransactionMethod("Destroy") {
|
||||
return nil // success
|
||||
} else {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
}
|
||||
case <-util.TimeAfterOrBlock(timeout):
|
||||
if finished {
|
||||
log.Println("PackageKit: Timeout: InstallPackages: Waiting for 'Destroy'")
|
||||
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
|
||||
return nil // got tired of waiting for Destroy
|
||||
}
|
||||
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %v", strings.Join(packageIDs, ", "))
|
||||
return fmt.Errorf("timeout installing packages: %s", strings.Join(packageIDs, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemovePackages removes a list of packages by packageID.
|
||||
func (bus *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
|
||||
var allowDeps = true // TODO: configurable
|
||||
var autoremove = false // unsupported on GNU/Linux
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
||||
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeSignals()
|
||||
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -464,12 +498,12 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||
// a package was installed...
|
||||
continue loop
|
||||
@@ -480,7 +514,7 @@ loop:
|
||||
// should already be broken
|
||||
break loop
|
||||
} else {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -488,18 +522,22 @@ loop:
|
||||
}
|
||||
|
||||
// UpdatePackages updates a list of packages to versions that are specified.
|
||||
func (bus *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
func (obj *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer removeSignals()
|
||||
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -509,12 +547,12 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||
} else if signal.Name == FmtTransactionMethod("Finished") {
|
||||
// TODO: should we wait for the Destroy signal?
|
||||
@@ -523,28 +561,33 @@ loop:
|
||||
// should already be broken
|
||||
break loop
|
||||
} else {
|
||||
return fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return fmt.Errorf("error in body: %v", signal.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
|
||||
func (bus *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
||||
// GetFilesByPackageID gets the list of files that are contained inside a list
|
||||
// of packageIDs.
|
||||
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
||||
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
|
||||
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer removeSignals()
|
||||
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
||||
if call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
@@ -557,12 +600,12 @@ loop:
|
||||
case signal := <-ch:
|
||||
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
err = fmt.Errorf("error in body: %v", signal.Body)
|
||||
return
|
||||
|
||||
// one signal returned per packageID found...
|
||||
@@ -587,7 +630,7 @@ loop:
|
||||
// should already be broken
|
||||
break loop
|
||||
} else {
|
||||
err = fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
err = fmt.Errorf("error in body: %v", signal.Body)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -595,23 +638,28 @@ loop:
|
||||
return
|
||||
}
|
||||
|
||||
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
|
||||
func (bus *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||
if Debug {
|
||||
log.Println("PackageKit: GetUpdates()")
|
||||
// GetUpdates gets a list of packages that are installed and which can be
|
||||
// updated, mod filter.
|
||||
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||
if obj.Debug {
|
||||
obj.Logf("GetUpdates()")
|
||||
}
|
||||
packageIDs := []string{}
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ?
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer removeSignals()
|
||||
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
||||
if call.Err != nil {
|
||||
return nil, call.Err
|
||||
}
|
||||
@@ -621,12 +669,12 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
if signal.Name == FmtTransactionMethod("ErrorCode") {
|
||||
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return nil, fmt.Errorf("error in body: %v", signal.Body)
|
||||
} else if signal.Name == FmtTransactionMethod("Package") {
|
||||
|
||||
//pkg_int, ok := signal.Body[0].(int)
|
||||
@@ -649,7 +697,7 @@ loop:
|
||||
// should already be broken
|
||||
break loop
|
||||
} else {
|
||||
return nil, fmt.Errorf("PackageKit: Error: %v", signal.Body)
|
||||
return nil, fmt.Errorf("error in body: %v", signal.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -660,7 +708,7 @@ loop:
|
||||
// outside mgmt. The packageMap input has the package names as keys and
|
||||
// requested states as values. These states can be: installed, uninstalled,
|
||||
// newest or a requested version str.
|
||||
func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
|
||||
func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
|
||||
count := 0
|
||||
packages := make([]string, len(packageMap))
|
||||
for k := range packageMap { // lol, golang has no hash.keys() function!
|
||||
@@ -672,12 +720,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
filter += PkFilterEnumArch // always search in our arch
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Printf("PackageKit: PackagesToPackageIDs(): %v", strings.Join(packages, ", "))
|
||||
if obj.Debug {
|
||||
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
|
||||
}
|
||||
resolved, e := bus.ResolvePackages(packages, filter)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("Resolve error: %v", e)
|
||||
resolved, err := obj.ResolvePackages(packages, filter)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error resolving")
|
||||
}
|
||||
|
||||
found := make([]bool, count) // default false
|
||||
@@ -692,13 +740,16 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
|
||||
for _, packageID := range resolved {
|
||||
index = -1
|
||||
//log.Printf("* %v", packageID)
|
||||
//obj.Logf("* %v", packageID)
|
||||
// format is: name;version;arch;data
|
||||
s := strings.Split(packageID, ";")
|
||||
//if len(s) != 4 { continue } // this would be a bug!
|
||||
pkg, ver, arch, data := s[0], s[1], s[2], s[3]
|
||||
// we might need to allow some of this, eg: i386 .deb on amd64
|
||||
if !IsMyArch(arch) {
|
||||
b, err := IsMyArch(arch)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "arch error")
|
||||
} else if !b {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -712,7 +763,7 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
}
|
||||
state := packageMap[pkg] // lookup the requested state/version
|
||||
if state == "" {
|
||||
return nil, fmt.Errorf("Empty package state for %v", pkg)
|
||||
return nil, fmt.Errorf("empty package state for: `%s`", pkg)
|
||||
}
|
||||
found[index] = true
|
||||
stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string
|
||||
@@ -748,12 +799,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
// to be done, and if so, anything that needs updating isn't newest!
|
||||
// if something isn't installed, we can't verify it with this method
|
||||
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
||||
updates, e := bus.GetUpdates(filter)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("Updates error: %v", e)
|
||||
updates, err := obj.GetUpdates(filter)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "updates error")
|
||||
}
|
||||
for _, packageID := range updates {
|
||||
//log.Printf("* %v", packageID)
|
||||
//obj.Logf("* %v", packageID)
|
||||
// format is: name;version;arch;data
|
||||
s := strings.Split(packageID, ";")
|
||||
//if len(s) != 4 { continue } // this would be a bug!
|
||||
@@ -792,15 +843,15 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
}
|
||||
|
||||
// we _could_ do a second resolve and then parse like this...
|
||||
//resolved, e := bus.ResolvePackages(..., filter+PkFilterEnumNewest)
|
||||
//resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest)
|
||||
// but that's basically what recursion here could do too!
|
||||
if len(checkPackages) > 0 {
|
||||
if Debug {
|
||||
log.Printf("PackageKit: PackagesToPackageIDs(): Recurse: %v", strings.Join(checkPackages, ", "))
|
||||
if obj.Debug {
|
||||
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
|
||||
}
|
||||
recursion, e = bus.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("Recursion error: %v", e)
|
||||
recursion, err = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "recursion error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -830,37 +881,39 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterPackageIDs returns a list of packageIDs which match the set of package names in packages.
|
||||
// FilterPackageIDs returns a list of packageIDs which match the set of package
|
||||
// names in packages.
|
||||
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
|
||||
result := []string{}
|
||||
for _, k := range packages {
|
||||
obj, ok := m[k] // lookup single package
|
||||
p, ok := m[k] // lookup single package
|
||||
// package doesn't exist, this is an error!
|
||||
if !ok || !obj.Found || obj.PackageID == "" {
|
||||
if !ok || !p.Found || p.PackageID == "" {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
result = append(result, obj.PackageID)
|
||||
result = append(result, p.PackageID)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterState returns a map of whether each package queried matches the particular state.
|
||||
// FilterState returns a map of whether each package queried matches the
|
||||
// particular state.
|
||||
func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) {
|
||||
result = make(map[string]bool)
|
||||
pkgs := []string{} // bad pkgs that don't have a bool state
|
||||
for _, k := range packages {
|
||||
obj, ok := m[k] // lookup single package
|
||||
p, ok := m[k] // lookup single package
|
||||
// package doesn't exist, this is an error!
|
||||
if !ok || !obj.Found {
|
||||
if !ok || !p.Found {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
var b bool
|
||||
if state == "installed" {
|
||||
b = obj.Installed
|
||||
b = p.Installed
|
||||
} else if state == "uninstalled" {
|
||||
b = !obj.Installed
|
||||
b = !p.Installed
|
||||
} else if state == "newest" {
|
||||
b = obj.Newest
|
||||
b = p.Newest
|
||||
} else {
|
||||
// we can't filter "version" state in this function
|
||||
pkgs = append(pkgs, k)
|
||||
@@ -869,28 +922,29 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
||||
result[k] = b // save
|
||||
}
|
||||
if len(pkgs) > 0 {
|
||||
err = fmt.Errorf("can't filter non-boolean state on: %v", strings.Join(pkgs, ","))
|
||||
err = fmt.Errorf("can't filter non-boolean state on: %s", strings.Join(pkgs, ","))
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FilterPackageState returns all packages that are in package and match the specific state.
|
||||
// FilterPackageState returns all packages that are in package and match the
|
||||
// specific state.
|
||||
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
|
||||
result = []string{}
|
||||
for _, k := range packages {
|
||||
obj, ok := m[k] // lookup single package
|
||||
p, ok := m[k] // lookup single package
|
||||
// package doesn't exist, this is an error!
|
||||
if !ok || !obj.Found {
|
||||
if !ok || !p.Found {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
b := false
|
||||
if state == "installed" && obj.Installed {
|
||||
if state == "installed" && p.Installed {
|
||||
b = true
|
||||
} else if state == "uninstalled" && !obj.Installed {
|
||||
} else if state == "uninstalled" && !p.Installed {
|
||||
b = true
|
||||
} else if state == "newest" && obj.Newest {
|
||||
} else if state == "newest" && p.Newest {
|
||||
b = true
|
||||
} else if state == obj.Version {
|
||||
} else if state == p.Version {
|
||||
b = true
|
||||
}
|
||||
if b {
|
||||
@@ -900,7 +954,8 @@ func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string,
|
||||
return result, err
|
||||
}
|
||||
|
||||
// FlagInData asks whether a flag exists inside the data portion of a packageID field?
|
||||
// FlagInData asks whether a flag exists inside the data portion of a packageID
|
||||
// field?
|
||||
func FlagInData(flag, data string) bool {
|
||||
flags := strings.Split(data, ":")
|
||||
for _, f := range flags {
|
||||
@@ -917,14 +972,14 @@ func FmtTransactionMethod(method string) string {
|
||||
}
|
||||
|
||||
// IsMyArch determines if a PackageKit architecture matches the current os arch.
|
||||
func IsMyArch(arch string) bool {
|
||||
func IsMyArch(arch string) (bool, error) {
|
||||
goarch, ok := PkArchMap[arch]
|
||||
if !ok {
|
||||
// if you get this error, please update the PkArchMap const
|
||||
log.Fatalf("PackageKit: Arch '%v', not found!", arch)
|
||||
return false, fmt.Errorf("arch '%s', not found", arch)
|
||||
}
|
||||
if goarch == "ANY" { // special value that corresponds to noarch
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
return goarch == runtime.GOARCH
|
||||
return goarch == runtime.GOARCH, nil
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -21,19 +21,19 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("password", func() Res { return &PasswordRes{} })
|
||||
engine.RegisterResource("password", func() engine.Res { return &PasswordRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -43,43 +43,54 @@ const (
|
||||
|
||||
// PasswordRes is a no-op resource that returns a random password string.
|
||||
type PasswordRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
// TODO: it could be useful to group our tokens into a single write, and
|
||||
// as a result, we save inotify watches too!
|
||||
//traits.Groupable // TODO: this is doable, but probably not very useful
|
||||
traits.Refreshable
|
||||
traits.Sendable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// FIXME: is uint16 too big?
|
||||
Length uint16 `yaml:"length"` // number of characters to return
|
||||
Saved bool // this caches the password in the clear locally
|
||||
CheckRecovery bool // recovery from integrity checks by re-generating
|
||||
Password *string // the generated password, read only, do not set!
|
||||
Length uint16 `yaml:"length"` // number of characters to return
|
||||
Saved bool // this caches the password in the clear locally
|
||||
CheckRecovery bool // recovery from integrity checks by re-generating
|
||||
|
||||
path string // the path to local storage
|
||||
recWatcher *recwatch.RecWatcher
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *PasswordRes) Default() Res {
|
||||
func (obj *PasswordRes) Default() engine.Res {
|
||||
return &PasswordRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
Length: 64, // safe default
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *PasswordRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init generates a new password for this resource if one was not provided. It
|
||||
// will save this into a local file. It will load it back in from previous runs.
|
||||
func (obj *PasswordRes) Init() error {
|
||||
// Init runs some startup code for this resource. It generates a new password
|
||||
// for this resource if one was not provided. It will save this into a local
|
||||
// file. It will load it back in from previous runs.
|
||||
func (obj *PasswordRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
dir, err := obj.VarDir("")
|
||||
dir, err := obj.init.VarDir("")
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||
}
|
||||
obj.path = path.Join(dir, "password") // return a unique file
|
||||
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *PasswordRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *PasswordRes) read() (string, error) {
|
||||
@@ -170,13 +181,9 @@ func (obj *PasswordRes) Watch() error {
|
||||
}
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
// NOTE: this part is very similar to the file resource code
|
||||
@@ -188,30 +195,25 @@ func (obj *PasswordRes) Watch() error {
|
||||
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Password resource. Does nothing, returns happy!
|
||||
func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
|
||||
var refresh = obj.Refresh() // do we have a pending reload to apply?
|
||||
var exists = true // does the file (aka the token) exist?
|
||||
var generate bool // do we need to generate a new password?
|
||||
var write bool // do we need to write out to disk?
|
||||
func (obj *PasswordRes) CheckApply(apply bool) (bool, error) {
|
||||
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||
var exists = true // does the file (aka the token) exist?
|
||||
var generate bool // do we need to generate a new password?
|
||||
var write bool // do we need to write out to disk?
|
||||
|
||||
password, err := obj.read() // password might be empty if just a token
|
||||
if err != nil {
|
||||
@@ -226,7 +228,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if !obj.CheckRecovery {
|
||||
return false, errwrap.Wrapf(err, "check failed")
|
||||
}
|
||||
log.Printf("%s: Integrity check failed", obj)
|
||||
obj.init.Logf("integrity check failed")
|
||||
generate = true // okay to build a new one
|
||||
write = true // make sure to write over the old one
|
||||
}
|
||||
@@ -240,9 +242,9 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
// stored password isn't consistent with memory
|
||||
if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
||||
write = true
|
||||
}
|
||||
//if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
||||
// write = true
|
||||
//}
|
||||
|
||||
if !refresh && exists && !generate && !write { // nothing to do, done!
|
||||
return true, nil
|
||||
@@ -260,13 +262,18 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
// generate the actual password
|
||||
var err error
|
||||
log.Printf("%s: Generating new password...", obj)
|
||||
obj.init.Logf("generating new password...")
|
||||
if password, err = obj.generate(); err != nil { // generate one!
|
||||
return false, errwrap.Wrapf(err, "could not generate password")
|
||||
}
|
||||
}
|
||||
|
||||
obj.Password = &password // save in memory
|
||||
// send
|
||||
if err := obj.init.Send(&PasswordSends{
|
||||
Password: &password,
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var output string // the string to write out
|
||||
|
||||
@@ -277,7 +284,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
output = password
|
||||
}
|
||||
// write either an empty token, or the password
|
||||
log.Printf("%s: Writing password token...", obj)
|
||||
obj.init.Logf("writing password token...")
|
||||
if _, err := obj.write(output); err != nil {
|
||||
return false, errwrap.Wrapf(err, "can't write to file")
|
||||
}
|
||||
@@ -286,64 +293,62 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// PasswordUID is the UID struct for PasswordRes.
|
||||
type PasswordUID struct {
|
||||
BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *PasswordRes) UIDs() []ResUID {
|
||||
x := &PasswordUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *PasswordRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*PasswordRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return false // TODO: this is doable, but probably not very useful
|
||||
// TODO: it could be useful to group our tokens into a single write, and
|
||||
// as a result, we save inotify watches too!
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *PasswordRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PasswordRes) Cmp(r engine.Res) error {
|
||||
// we can only compare PasswordRes to others of the same resource kind
|
||||
res, ok := r.(*PasswordRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Length != res.Length {
|
||||
return false
|
||||
return fmt.Errorf("the Length differs")
|
||||
}
|
||||
// TODO: we *could* optimize by allowing CheckApply to move from
|
||||
// saved->!saved, by removing the file, but not likely worth it!
|
||||
if obj.Saved != res.Saved {
|
||||
return false
|
||||
return fmt.Errorf("the Saved differs")
|
||||
}
|
||||
if obj.CheckRecovery != res.CheckRecovery {
|
||||
return false
|
||||
return fmt.Errorf("the CheckRecovery differs")
|
||||
}
|
||||
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// PasswordUID is the UID struct for PasswordRes.
|
||||
type PasswordUID struct {
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *PasswordRes) UIDs() []engine.ResUID {
|
||||
x := &PasswordUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// PasswordSends is the struct of data which is sent after a successful Apply.
|
||||
type PasswordSends struct {
|
||||
// Password is the generated password being sent.
|
||||
Password *string `lang:"password"`
|
||||
// Hashing is the algorithm used for this password. Empty is plain text.
|
||||
Hashing string // TODO: implement me
|
||||
}
|
||||
|
||||
// Sends represents the default struct of values we can send using Send/Recv.
|
||||
func (obj *PasswordRes) Sends() interface{} {
|
||||
return &PasswordSends{
|
||||
Password: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes PasswordRes // indirection to avoid infinite recursion
|
||||
|
||||
329
engine/resources/pippet.go
Normal file
329
engine/resources/pippet.go
Normal file
@@ -0,0 +1,329 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
var pippetReceiverInstance *pippetReceiver
|
||||
var pippetReceiverOnce sync.Once
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("pippet", func() engine.Res { return &PippetRes{} })
|
||||
}
|
||||
|
||||
// PippetRes is a wrapper resource for puppet. It implements the functional
|
||||
// equivalent of an exec resource that calls "puppet resource <type> <title>
|
||||
// <params>", but offers superior performance through a long-running Puppet
|
||||
// process that receives resources through a pipe (hence the name).
|
||||
type PippetRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Type is the exact name of the wrapped Puppet resource type, e.g.
|
||||
// "file", "mount". This needs not be a core type. It can be a type
|
||||
// from a module. The Puppet installation local to the mgmt agent
|
||||
// machine must be able recognize it. It has to be a native type though,
|
||||
// as opposed to defined types from your Puppet manifest code.
|
||||
Type string `yaml:"type" json:"type"`
|
||||
// Title is used by Puppet as the resource title. Puppet will often
|
||||
// assign special meaning to the title, e.g. use it as the path for a
|
||||
// file resource, or the name of a package.
|
||||
Title string `yaml:"title" json:"title"`
|
||||
// Params is expected to be a hash in YAML format, pairing resource
|
||||
// parameter names with their respective values, e.g. { ensure: present
|
||||
// }
|
||||
Params string `yaml:"params" json:"params"`
|
||||
|
||||
runner *pippetReceiver
|
||||
}
|
||||
|
||||
// Default returns an example Pippet resource.
|
||||
func (obj *PippetRes) Default() engine.Res {
|
||||
return &PippetRes{
|
||||
Params: "{}", // use an empty params hash per default
|
||||
}
|
||||
}
|
||||
|
||||
// Validate never errors out. We don't know the set of potential types that
|
||||
// Puppet supports. Resource names are arbitrary. We cannot really validate the
|
||||
// parameter YAML, because we cannot assume that it can be unmarshalled into a
|
||||
// map[string]string; Puppet supports complex parameter values.
|
||||
func (obj *PippetRes) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init makes sure that the PippetReceiver object is initialized.
|
||||
func (obj *PippetRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
obj.runner = getPippetReceiverInstance()
|
||||
return obj.runner.Register()
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *PippetRes) Close() error {
|
||||
return obj.runner.Unregister()
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *PippetRes) Watch() error {
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
select {
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckApply synchronizes the resource if required.
|
||||
func (obj *PippetRes) CheckApply(apply bool) (bool, error) {
|
||||
changed, err := applyPippetRes(obj.runner, obj)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("pippet: %s[%s]: ERROR - %v", obj.Type, obj.Title, err)
|
||||
}
|
||||
return !changed, nil
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PippetRes) Cmp(r engine.Res) error {
|
||||
res, ok := r.(*PippetRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Type != res.Type {
|
||||
return fmt.Errorf("the Type param differs")
|
||||
}
|
||||
|
||||
if obj.Title != res.Title {
|
||||
return fmt.Errorf("the Title param differs")
|
||||
}
|
||||
|
||||
// FIXME: This is a lie. Parameter lists can be equivalent but not
|
||||
// lexically identical (e.g. whitespace differences, parameter order).
|
||||
// This is difficult to handle because we cannot casually unmarshall the
|
||||
// YAML content.
|
||||
if obj.Params != res.Params {
|
||||
return fmt.Errorf("the Param param differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PippetUID is the UID struct for PippetRes.
|
||||
type PippetUID struct {
|
||||
engine.BaseUID
|
||||
resourceType string
|
||||
resourceTitle string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *PippetRes) UIDs() []engine.ResUID {
|
||||
x := &PippetUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
resourceType: obj.Type,
|
||||
resourceTitle: obj.Title,
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *PippetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes PippetRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*PippetRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to PippetRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = PippetRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// PippetRunner is the interface used to communicate with the PippetReceiver
|
||||
// object. Its main purpose is dependency injection.
|
||||
type PippetRunner interface {
|
||||
LockApply()
|
||||
UnlockApply()
|
||||
InputStream() io.WriteCloser
|
||||
OutputStream() io.ReadCloser
|
||||
}
|
||||
|
||||
// PippetResult is the structured return value type for the PippetReceiver's
|
||||
// Apply function.
|
||||
type PippetResult struct {
|
||||
Error bool
|
||||
Failed bool
|
||||
Changed bool
|
||||
Exception string
|
||||
}
|
||||
|
||||
// GetPippetReceiverInstance returns a pointer to the PippetReceiver object. The
|
||||
// PippetReceiver is supposed to be a singleton object. The pippet resource code
|
||||
// should always use the PippetReceiverInstance function to gain access to the
|
||||
// pippetReceiver object. Other objects of type pippetReceiver should not be
|
||||
// created.
|
||||
func getPippetReceiverInstance() *pippetReceiver {
|
||||
for pippetReceiverInstance == nil {
|
||||
pippetReceiverOnce.Do(func() { pippetReceiverInstance = &pippetReceiver{} })
|
||||
}
|
||||
return pippetReceiverInstance
|
||||
}
|
||||
|
||||
type pippetReceiver struct {
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
registerMutex sync.Mutex
|
||||
applyMutex sync.Mutex
|
||||
registered int
|
||||
}
|
||||
|
||||
// Init runs the Puppet process that will perform the work of synchronizing
|
||||
// resources that are sent to its stdin. The process will keep running until
|
||||
// Close is called. Init should not be called directly. It is implicitly called
|
||||
// by the Register function.
|
||||
func (obj *pippetReceiver) Init() error {
|
||||
cmd := exec.Command("puppet", "yamlresource", "receive", "--color=no")
|
||||
var err error
|
||||
obj.stdin, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
obj.stdout, err = cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return errwrap.Append(err, obj.stdin.Close())
|
||||
}
|
||||
if err = cmd.Start(); err != nil {
|
||||
return errwrap.Append(err, obj.stdin.Close())
|
||||
}
|
||||
buf := make([]byte, 80)
|
||||
if _, err = obj.stdout.Read(buf); err != nil {
|
||||
return errwrap.Append(err, obj.stdin.Close())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register should be called by any user (i.e., any pippet resource) before
|
||||
// using the PippetRunner functions on this receiver object. Register implicitly
|
||||
// takes care of calling Init if required.
|
||||
func (obj *pippetReceiver) Register() error {
|
||||
obj.registerMutex.Lock()
|
||||
defer obj.registerMutex.Unlock()
|
||||
obj.registered = obj.registered + 1
|
||||
if obj.registered > 1 {
|
||||
return nil
|
||||
}
|
||||
// count was increased from 0 to 1, we need to (re-)init
|
||||
var err error
|
||||
if err = obj.Init(); err != nil {
|
||||
obj.registered = 0
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Unregister should be called by any object that registered itself using the
|
||||
// Register function, and which no longer needs the receiver. This should
|
||||
// typically happen at closing time of the pippet resource that registered
|
||||
// itself. Unregister implicitly calls Close in case all registered resources
|
||||
// have unregistered.
|
||||
func (obj *pippetReceiver) Unregister() error {
|
||||
obj.registerMutex.Lock()
|
||||
defer obj.registerMutex.Unlock()
|
||||
obj.registered = obj.registered - 1
|
||||
if obj.registered == 0 {
|
||||
return obj.Close()
|
||||
}
|
||||
if obj.registered < 0 {
|
||||
return fmt.Errorf("pippet runner: ERROR: unregistered more resources than were registered")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LockApply locks the pippetReceiver's mutex for an "Apply" transaction.
|
||||
func (obj *pippetReceiver) LockApply() {
|
||||
obj.applyMutex.Lock()
|
||||
}
|
||||
|
||||
// UnlockApply unlocks the pippetReceiver's mutex for an "Apply" transaction.
|
||||
func (obj *pippetReceiver) UnlockApply() {
|
||||
obj.applyMutex.Unlock()
|
||||
}
|
||||
|
||||
// InputStream returns the pippetReceiver's pipe writer.
|
||||
func (obj *pippetReceiver) InputStream() io.WriteCloser {
|
||||
return obj.stdin
|
||||
}
|
||||
|
||||
// OutputStream returns the pippetReceiver's pipe reader.
|
||||
func (obj *pippetReceiver) OutputStream() io.ReadCloser {
|
||||
return obj.stdout
|
||||
}
|
||||
|
||||
// Close stops the backend puppet process by closing its stdin handle. It should
|
||||
// not be called directly. It is implicitly called by the Unregister function if
|
||||
// appropriate.
|
||||
func (obj *pippetReceiver) Close() error {
|
||||
return obj.stdin.Close()
|
||||
}
|
||||
|
||||
// applyPippetRes does the actual work of making Puppet synchronize a resource.
|
||||
func applyPippetRes(runner PippetRunner, resource *PippetRes) (bool, error) {
|
||||
runner.LockApply()
|
||||
defer runner.UnlockApply()
|
||||
if err := json.NewEncoder(runner.InputStream()).Encode(resource); err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to send resource to puppet")
|
||||
}
|
||||
|
||||
result := PippetResult{
|
||||
Error: true,
|
||||
Exception: "missing output fields",
|
||||
}
|
||||
if err := json.NewDecoder(runner.OutputStream()).Decode(&result); err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to read response from puppet")
|
||||
}
|
||||
|
||||
if result.Error {
|
||||
return false, fmt.Errorf("puppet did not sync: %s", result.Exception)
|
||||
}
|
||||
if result.Failed {
|
||||
return false, fmt.Errorf("puppet failed to sync")
|
||||
}
|
||||
return result.Changed, nil
|
||||
}
|
||||
136
engine/resources/pippet_test.go
Normal file
136
engine/resources/pippet_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// 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 3 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/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type nullWriteCloser struct {
|
||||
}
|
||||
|
||||
type fakePippetReceiver struct {
|
||||
stdin nullWriteCloser
|
||||
stdout *io.PipeReader
|
||||
Locked bool
|
||||
}
|
||||
|
||||
func (obj nullWriteCloser) Write(data []byte) (int, error) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (obj nullWriteCloser) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (obj *fakePippetReceiver) LockApply() {
|
||||
obj.Locked = true
|
||||
}
|
||||
|
||||
func (obj *fakePippetReceiver) UnlockApply() {
|
||||
obj.Locked = false
|
||||
}
|
||||
|
||||
func (obj *fakePippetReceiver) InputStream() io.WriteCloser {
|
||||
return obj.stdin
|
||||
}
|
||||
|
||||
func (obj *fakePippetReceiver) OutputStream() io.ReadCloser {
|
||||
return obj.stdout
|
||||
}
|
||||
|
||||
func newFakePippetReceiver(jsonTestOutput string) *fakePippetReceiver {
|
||||
output, input := io.Pipe()
|
||||
|
||||
result := &fakePippetReceiver{
|
||||
stdout: output,
|
||||
}
|
||||
|
||||
go func() {
|
||||
// this will appear on the fake stdout
|
||||
input.Write([]byte(jsonTestOutput))
|
||||
}()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var pippetTestRes = &PippetRes{
|
||||
Type: "notify",
|
||||
Title: "testmessage",
|
||||
Params: `{msg: "This is a test"}`,
|
||||
}
|
||||
|
||||
func TestNormalPuppetOutput(t *testing.T) {
|
||||
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true,"noop":false,"error":false,"exception":null}`)
|
||||
changed, err := applyPippetRes(r, pippetTestRes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("normal Puppet output led to an apply error: %v", err)
|
||||
}
|
||||
|
||||
if !changed {
|
||||
t.Errorf("return values of applyPippetRes did not reflect the changed state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnchangedPuppetOutput(t *testing.T) {
|
||||
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":false,"exception":null}`)
|
||||
changed, err := applyPippetRes(r, pippetTestRes)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("normal Puppet output led to an apply error: %v", err)
|
||||
}
|
||||
|
||||
if changed {
|
||||
t.Errorf("return values of applyPippetRes did not reflect the changed state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailingPuppetOutput(t *testing.T) {
|
||||
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":false,"noop":false,"error":true,"exception":"I failed!"}`)
|
||||
_, err := applyPippetRes(r, pippetTestRes)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("failing Puppet output led to an apply error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyPuppetOutput(t *testing.T) {
|
||||
t.Skip("empty output will currently make the application (and the test) hang")
|
||||
}
|
||||
|
||||
func TestPartialPuppetOutput(t *testing.T) {
|
||||
r := newFakePippetReceiver(`{"resource":"Notify[test]","failed":false,"changed":true}`)
|
||||
_, err := applyPippetRes(r, pippetTestRes)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("partial Puppet output did not lead to an apply error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMalformedPuppetOutput(t *testing.T) {
|
||||
r := newFakePippetReceiver(`oops something went wrong!!1!eleven`)
|
||||
_, err := applyPippetRes(r, pippetTestRes)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("malformed Puppet output did not lead to an apply error")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,23 +19,42 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/resources/packagekit"
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/resources/packagekit"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("pkg", func() Res { return &PkgRes{} })
|
||||
engine.RegisterResource("pkg", func() engine.Res { return &PkgRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
// PkgStateInstalled is the string that represents that the package
|
||||
// should be installed.
|
||||
PkgStateInstalled = "installed"
|
||||
|
||||
// PkgStateUninstalled is the string that represents that the package
|
||||
// should be uninstalled.
|
||||
PkgStateUninstalled = "uninstalled"
|
||||
|
||||
// PkgStateNewest is the string that represents that the package should
|
||||
// be installed in the newest available version.
|
||||
PkgStateNewest = "newest"
|
||||
)
|
||||
|
||||
// PkgRes is a package resource for packagekit.
|
||||
type PkgRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Edgeable
|
||||
traits.Groupable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
State string `yaml:"state"` // state: installed, uninstalled, newest, <version>
|
||||
AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed?
|
||||
AllowNonFree bool `yaml:"allownonfree"` // allow nonfree packages to be found?
|
||||
@@ -45,12 +64,9 @@ type PkgRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *PkgRes) Default() Res {
|
||||
func (obj *PkgRes) Default() engine.Res {
|
||||
return &PkgRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
State: "installed", // i think this is preferable to "latest"
|
||||
State: PkgStateInstalled, // this *is* preferable to "newest"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,15 +75,16 @@ func (obj *PkgRes) Validate() error {
|
||||
if obj.State == "" {
|
||||
return fmt.Errorf("state cannot be empty")
|
||||
}
|
||||
if obj.State == "latest" {
|
||||
return fmt.Errorf("state is invalid, did you mean `newest` ?")
|
||||
}
|
||||
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *PkgRes) Init() error {
|
||||
if err := obj.BaseRes.Init(); err != nil { // call base init, b/c we're overriding
|
||||
return err
|
||||
}
|
||||
func (obj *PkgRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
if obj.fileList == nil {
|
||||
if err := obj.populateFileList(); err != nil {
|
||||
@@ -78,8 +95,13 @@ func (obj *PkgRes) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
// It uses the PackageKit UpdatesChanged signal to watch for changes.
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *PkgRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events. It
|
||||
// uses the PackageKit UpdatesChanged signal to watch for changes.
|
||||
// TODO: https://github.com/hughsie/PackageKit/issues/109
|
||||
// TODO: https://github.com/hughsie/PackageKit/issues/110
|
||||
func (obj *PkgRes) Watch() error {
|
||||
@@ -88,30 +110,29 @@ func (obj *PkgRes) Watch() error {
|
||||
return fmt.Errorf("can't connect to PackageKit bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
bus.Debug = obj.init.Debug
|
||||
bus.Logf = func(format string, v ...interface{}) {
|
||||
obj.init.Logf("packagekit: "+format, v...)
|
||||
}
|
||||
|
||||
ch, err := bus.WatchChanges()
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error adding signal match")
|
||||
}
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
}
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching...", obj.fmtNames(obj.getNames()))
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("%s: Watching...", obj.fmtNames(obj.getNames()))
|
||||
}
|
||||
|
||||
select {
|
||||
case event := <-ch:
|
||||
// FIXME: ask packagekit for info on what packages changed
|
||||
if obj.debug {
|
||||
log.Printf("%s: Event: %v", obj.fmtNames(obj.getNames()), event.Name)
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event(%s): %s", event.Name, obj.fmtNames(obj.getNames()))
|
||||
}
|
||||
|
||||
// since the chan is buffered, remove any supplemental
|
||||
@@ -121,20 +142,15 @@ func (obj *PkgRes) Watch() error {
|
||||
}
|
||||
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
//obj.StateOK(false) // these events don't invalidate state
|
||||
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,22 +158,22 @@ func (obj *PkgRes) Watch() error {
|
||||
// get list of names when grouped or not
|
||||
func (obj *PkgRes) getNames() []string {
|
||||
if g := obj.GetGroup(); len(g) > 0 { // grouped elements
|
||||
names := []string{obj.GetName()}
|
||||
names := []string{obj.Name()}
|
||||
for _, x := range g {
|
||||
pkg, ok := x.(*PkgRes) // convert from Res
|
||||
if ok {
|
||||
names = append(names, pkg.Name)
|
||||
names = append(names, pkg.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
return []string{obj.GetName()}
|
||||
return []string{obj.Name()}
|
||||
}
|
||||
|
||||
// pretty print for header values
|
||||
func (obj *PkgRes) fmtNames(names []string) string {
|
||||
if len(obj.GetGroup()) > 0 { // grouped elements
|
||||
return fmt.Sprintf("%s[autogroup:(%s)]", obj.GetKind(), strings.Join(names, ","))
|
||||
return fmt.Sprintf("%s[autogroup:(%s)]", obj.Kind(), strings.Join(names, ","))
|
||||
}
|
||||
return obj.String()
|
||||
}
|
||||
@@ -168,9 +184,9 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
|
||||
for _, x := range g {
|
||||
pkg, ok := x.(*PkgRes) // convert from Res
|
||||
if !ok {
|
||||
log.Fatalf("grouped member %v is not a %s", x, obj.GetKind())
|
||||
panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
|
||||
}
|
||||
result[pkg.Name] = pkg.State
|
||||
result[pkg.Name()] = pkg.State
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -178,11 +194,11 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
|
||||
|
||||
func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) {
|
||||
packageMap := obj.groupMappingHelper() // get the grouped values
|
||||
packageMap[obj.Name] = obj.State // key is pkg name, value is pkg state
|
||||
packageMap[obj.Name()] = obj.State // key is pkg name, value is pkg state
|
||||
var filter uint64 // initializes at the "zero" value of 0
|
||||
filter += packagekit.PkFilterEnumArch // always search in our arch (optional!)
|
||||
// we're requesting latest version, or to narrow down install choices!
|
||||
if obj.State == "newest" || obj.State == "installed" {
|
||||
// we're requesting newest version, or to narrow down install choices!
|
||||
if obj.State == PkgStateNewest || obj.State == PkgStateInstalled {
|
||||
// if we add this, we'll still see older packages if installed
|
||||
// this is an optimization, and is *optional*, this logic is
|
||||
// handled inside of PackagesToPackageIDs now automatically!
|
||||
@@ -196,7 +212,7 @@ func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packageki
|
||||
}
|
||||
result, err := bus.PackagesToPackageIDs(packageMap, filter)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "Can't run PackagesToPackageIDs")
|
||||
return nil, errwrap.Wrapf(err, "can't run PackagesToPackageIDs")
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -210,16 +226,26 @@ func (obj *PkgRes) populateFileList() error {
|
||||
return fmt.Errorf("can't connect to PackageKit bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
if obj.init != nil {
|
||||
bus.Debug = obj.init.Debug
|
||||
bus.Logf = func(format string, v ...interface{}) {
|
||||
obj.init.Logf("packagekit: "+format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
result, err := obj.pkgMappingHelper(bus)
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "the pkgMappingHelper failed")
|
||||
}
|
||||
|
||||
data, ok := result[obj.Name] // lookup single package (init does just one)
|
||||
data, ok := result[obj.Name()] // lookup single package (init does just one)
|
||||
// package doesn't exist, this is an error!
|
||||
if !ok || !data.Found {
|
||||
return fmt.Errorf("can't find package named '%s'", obj.Name)
|
||||
return fmt.Errorf("can't find package named '%s'", obj.Name())
|
||||
}
|
||||
if data.PackageID == "" {
|
||||
// this can happen if you specify a bad version like "latest"
|
||||
return fmt.Errorf("empty PackageID found for '%s'", obj.Name())
|
||||
}
|
||||
|
||||
packageIDs := []string{data.PackageID} // just one for now
|
||||
@@ -236,14 +262,18 @@ func (obj *PkgRes) populateFileList() error {
|
||||
|
||||
// CheckApply checks the resource state and applies the resource if the bool
|
||||
// input is true. It returns error info and if the state check passed or not.
|
||||
func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: Check", obj.fmtNames(obj.getNames()))
|
||||
func (obj *PkgRes) CheckApply(apply bool) (bool, error) {
|
||||
obj.init.Logf("Check: %s", obj.fmtNames(obj.getNames()))
|
||||
|
||||
bus := packagekit.NewBus()
|
||||
if bus == nil {
|
||||
return false, fmt.Errorf("can't connect to PackageKit bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
bus.Debug = obj.init.Debug
|
||||
bus.Logf = func(format string, v ...interface{}) {
|
||||
obj.init.Logf("packagekit: "+format, v...)
|
||||
}
|
||||
|
||||
result, err := obj.pkgMappingHelper(bus)
|
||||
if err != nil {
|
||||
@@ -251,7 +281,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
packageMap := obj.groupMappingHelper() // map[string]string
|
||||
packageList := []string{obj.Name}
|
||||
packageList := []string{obj.Name()}
|
||||
packageList = append(packageList, util.StrMapKeys(packageMap)...)
|
||||
//stateList := []string{obj.State}
|
||||
//stateList = append(stateList, util.StrMapValues(packageMap)...)
|
||||
@@ -262,16 +292,16 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "the FilterState method failed")
|
||||
}
|
||||
data, _ := result[obj.Name] // if above didn't error, we won't either!
|
||||
data, _ := result[obj.Name()] // if above didn't error, we won't either!
|
||||
validState := util.BoolMapTrue(util.BoolMapValues(states))
|
||||
|
||||
// obj.State == "installed" || "uninstalled" || "newest" || "4.2-1.fc23"
|
||||
// obj.State == PkgStateInstalled || PkgStateUninstalled || PkgStateNewest || "4.2-1.fc23"
|
||||
switch obj.State {
|
||||
case "installed":
|
||||
case PkgStateInstalled:
|
||||
fallthrough
|
||||
case "uninstalled":
|
||||
case PkgStateUninstalled:
|
||||
fallthrough
|
||||
case "newest":
|
||||
case PkgStateNewest:
|
||||
if validState {
|
||||
return true, nil // state is correct, exit!
|
||||
}
|
||||
@@ -287,7 +317,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
// apply portion
|
||||
log.Printf("%s: Apply", obj.fmtNames(obj.getNames()))
|
||||
obj.init.Logf("Apply: %s", obj.fmtNames(obj.getNames()))
|
||||
readyPackages, err := packagekit.FilterPackageState(result, packageList, obj.State)
|
||||
if err != nil {
|
||||
return false, err // fail
|
||||
@@ -301,17 +331,17 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted
|
||||
}
|
||||
// apply correct state!
|
||||
log.Printf("%s: Set: %v...", obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())), obj.State)
|
||||
obj.init.Logf("Set(%s): %s...", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
|
||||
switch obj.State {
|
||||
case "uninstalled": // run remove
|
||||
case PkgStateUninstalled: // run remove
|
||||
// NOTE: packageID is different than when installed, because now
|
||||
// it has the "installed" flag added to the data portion if it!!
|
||||
// it has the "installed" flag added to the data portion of it!!
|
||||
err = bus.RemovePackages(packageIDs, transactionFlags)
|
||||
|
||||
case "newest": // TODO: isn't this the same operation as install, below?
|
||||
case PkgStateNewest: // TODO: isn't this the same operation as install, below?
|
||||
err = bus.UpdatePackages(packageIDs, transactionFlags)
|
||||
|
||||
case "installed":
|
||||
case PkgStateInstalled:
|
||||
fallthrough // same method as for "set specific version", below
|
||||
default: // version string
|
||||
err = bus.InstallPackages(packageIDs, transactionFlags)
|
||||
@@ -319,25 +349,116 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if err != nil {
|
||||
return false, err // fail
|
||||
}
|
||||
log.Printf("%s: Set: %v success!", obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())), obj.State)
|
||||
obj.init.Logf("Set(%s) success: %s", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
|
||||
return false, nil // success
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PkgRes) Cmp(r engine.Res) error {
|
||||
// we can only compare PkgRes to others of the same resource kind
|
||||
res, ok := r.(*PkgRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return fmt.Errorf("state differs: %s vs %s", obj.State, res.State)
|
||||
}
|
||||
|
||||
return obj.Adapts(res)
|
||||
}
|
||||
|
||||
// Adapts compares two resources and returns an error if they are not able to be
|
||||
// equivalently output compatible.
|
||||
func (obj *PkgRes) Adapts(r engine.CompatibleRes) error {
|
||||
res, ok := r.(*PkgRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
e := fmt.Errorf("state differs in an incompatible way: %s vs %s", obj.State, res.State)
|
||||
if obj.State == PkgStateUninstalled || res.State == PkgStateUninstalled {
|
||||
return e
|
||||
}
|
||||
if stateIsVersion(obj.State) || stateIsVersion(res.State) {
|
||||
return e
|
||||
}
|
||||
// one must be installed, and the other must be "newest"
|
||||
}
|
||||
|
||||
if obj.AllowUntrusted != res.AllowUntrusted {
|
||||
return fmt.Errorf("allowuntrusted differs: %t vs %t", obj.AllowUntrusted, res.AllowUntrusted)
|
||||
}
|
||||
if obj.AllowNonFree != res.AllowNonFree {
|
||||
return fmt.Errorf("allownonfree differs: %t vs %t", obj.AllowNonFree, res.AllowNonFree)
|
||||
}
|
||||
if obj.AllowUnsupported != res.AllowUnsupported {
|
||||
return fmt.Errorf("allowunsupported differs: %t vs %t", obj.AllowUnsupported, res.AllowUnsupported)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Merge returns the best equivalent of the two resources. They must satisfy the
|
||||
// Adapts test for this to work.
|
||||
func (obj *PkgRes) Merge(r engine.CompatibleRes) (engine.CompatibleRes, error) {
|
||||
res, ok := r.(*PkgRes)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
if err := obj.Adapts(r); err != nil {
|
||||
return nil, errwrap.Wrapf(err, "can't merge resources that aren't compatible")
|
||||
}
|
||||
|
||||
// modify the copy, not the original
|
||||
x, err := engine.ResCopy(obj) // don't call our .Copy() directly!
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, ok := x.(*PkgRes)
|
||||
if !ok {
|
||||
// bug!
|
||||
return nil, fmt.Errorf("res is not the same kind")
|
||||
}
|
||||
|
||||
// if these two were compatible then if they're not identical, then one
|
||||
// must be PkgStateNewest and the other is PkgStateInstalled, so we
|
||||
// upgrade to the best common denominator
|
||||
if obj.State != res.State {
|
||||
result.State = PkgStateNewest
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||
// TODO: should this copy internal state?
|
||||
func (obj *PkgRes) Copy() engine.CopyableRes {
|
||||
return &PkgRes{
|
||||
State: obj.State,
|
||||
AllowUntrusted: obj.AllowUntrusted,
|
||||
AllowNonFree: obj.AllowNonFree,
|
||||
AllowUnsupported: obj.AllowUnsupported,
|
||||
}
|
||||
}
|
||||
|
||||
// PkgUID is the main UID struct for PkgRes.
|
||||
type PkgUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string // pkg name
|
||||
state string // pkg state or "version"
|
||||
}
|
||||
|
||||
// PkgFileUID is the UID struct for PkgRes files.
|
||||
type PkgFileUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
path string // path of the file
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *PkgUID) IFF(uid ResUID) bool {
|
||||
func (obj *PkgUID) IFF(uid engine.ResUID) bool {
|
||||
res, ok := uid.(*PkgUID)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -349,16 +470,16 @@ func (obj *PkgUID) IFF(uid ResUID) bool {
|
||||
// PkgResAutoEdges holds the state of the auto edge generator.
|
||||
type PkgResAutoEdges struct {
|
||||
fileList []string
|
||||
svcUIDs []ResUID
|
||||
svcUIDs []engine.ResUID
|
||||
testIsNext bool // safety
|
||||
name string // saved data from PkgRes obj
|
||||
kind string
|
||||
}
|
||||
|
||||
// Next returns the next automatic edge.
|
||||
func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
func (obj *PkgResAutoEdges) Next() []engine.ResUID {
|
||||
if obj.testIsNext {
|
||||
log.Fatal("expecting a call to Test()")
|
||||
panic("expecting a call to Test()")
|
||||
}
|
||||
obj.testIsNext = true // set after all the errors paths are past
|
||||
|
||||
@@ -367,12 +488,12 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
return x
|
||||
}
|
||||
|
||||
var result []ResUID
|
||||
var result []engine.ResUID
|
||||
// return UID's for whatever is in obj.fileList
|
||||
for _, x := range obj.fileList {
|
||||
var reversed = false // cheat by passing a pointer
|
||||
result = append(result, &FileUID{
|
||||
BaseUID: BaseUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.name,
|
||||
Kind: obj.kind,
|
||||
Reversed: &reversed,
|
||||
@@ -383,25 +504,26 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
return result
|
||||
}
|
||||
|
||||
// Test gets results of the earlier Next() call, & returns if we should continue!
|
||||
// Test gets results of the earlier Next() call, & returns if we should
|
||||
// continue!
|
||||
func (obj *PkgResAutoEdges) Test(input []bool) bool {
|
||||
if !obj.testIsNext {
|
||||
log.Fatal("expecting a call to Next()")
|
||||
panic("expecting a call to Next()")
|
||||
}
|
||||
|
||||
// ack the svcUID's...
|
||||
if x := obj.svcUIDs; len(x) > 0 {
|
||||
if y := len(x); y != len(input) {
|
||||
log.Fatalf("expecting %d value(s)", y)
|
||||
panic(fmt.Sprintf("expecting %d value(s)", y))
|
||||
}
|
||||
obj.svcUIDs = []ResUID{} // empty
|
||||
obj.svcUIDs = []engine.ResUID{} // empty
|
||||
obj.testIsNext = false
|
||||
return true
|
||||
}
|
||||
|
||||
count := len(obj.fileList)
|
||||
if count != len(input) {
|
||||
log.Fatalf("expecting %d value(s)", count)
|
||||
panic(fmt.Sprintf("expecting %d value(s)", count))
|
||||
}
|
||||
obj.testIsNext = false // set after all the errors paths are past
|
||||
|
||||
@@ -436,7 +558,7 @@ func (obj *PkgResAutoEdges) Test(input []bool) bool {
|
||||
|
||||
// AutoEdges produces an object which generates a minimal pkg file optimization
|
||||
// sequence of edges.
|
||||
func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
|
||||
func (obj *PkgRes) AutoEdges() (engine.AutoEdge, error) {
|
||||
// in contrast with the FileRes AutoEdges() function which contains
|
||||
// more of the mechanics, most of the AutoEdge mechanics for the PkgRes
|
||||
// are contained in the Test() method! This design is completely okay!
|
||||
@@ -448,13 +570,13 @@ func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
|
||||
}
|
||||
|
||||
// add matches for any svc resources found in pkg definition!
|
||||
var svcUIDs []ResUID
|
||||
var svcUIDs []engine.ResUID
|
||||
for _, x := range ReturnSvcInFileList(obj.fileList) {
|
||||
var reversed = false
|
||||
svcUIDs = append(svcUIDs, &SvcUID{
|
||||
BaseUID: BaseUID{
|
||||
Name: obj.GetName(),
|
||||
Kind: obj.GetKind(),
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
name: x, // the svc name itself in the SvcUID object!
|
||||
@@ -464,25 +586,25 @@ func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
|
||||
return &PkgResAutoEdges{
|
||||
fileList: util.RemoveCommonFilePrefixes(obj.fileList), // clean start!
|
||||
svcUIDs: svcUIDs,
|
||||
testIsNext: false, // start with Next() call
|
||||
name: obj.GetName(), // save data for PkgResAutoEdges obj
|
||||
kind: obj.GetKind(),
|
||||
testIsNext: false, // start with Next() call
|
||||
name: obj.Name(), // save data for PkgResAutoEdges obj
|
||||
kind: obj.Kind(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *PkgRes) UIDs() []ResUID {
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *PkgRes) UIDs() []engine.ResUID {
|
||||
x := &PkgUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
state: obj.State,
|
||||
}
|
||||
result := []ResUID{x}
|
||||
result := []engine.ResUID{x}
|
||||
|
||||
for _, y := range obj.fileList {
|
||||
y := &PkgFileUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
path: y,
|
||||
}
|
||||
result = append(result, y)
|
||||
@@ -490,60 +612,28 @@ func (obj *PkgRes) UIDs() []ResUID {
|
||||
return result
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
// can these two resources be merged ?
|
||||
// (aka does this resource support doing so?)
|
||||
// will resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *PkgRes) GroupCmp(r Res) bool {
|
||||
// GroupCmp returns whether two resources can be grouped together or not. Can
|
||||
// these two resources be merged, aka, does this resource support doing so? Will
|
||||
// resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
|
||||
res, ok := r.(*PkgRes)
|
||||
if !ok {
|
||||
return false
|
||||
return fmt.Errorf("resource is not the same kind")
|
||||
}
|
||||
objStateIsVersion := (obj.State != "installed" && obj.State != "uninstalled" && obj.State != "newest") // must be a ver. string
|
||||
resStateIsVersion := (res.State != "installed" && res.State != "uninstalled" && res.State != "newest") // must be a ver. string
|
||||
if objStateIsVersion || resStateIsVersion {
|
||||
// TODO: what should we do about the empty string?
|
||||
if stateIsVersion(obj.State) || stateIsVersion(res.State) {
|
||||
// can't merge specific version checks atm
|
||||
return false
|
||||
return fmt.Errorf("resource uses a version string")
|
||||
}
|
||||
// FIXME: keep it simple for now, only merge same states
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
return fmt.Errorf("resource is of a different state")
|
||||
}
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *PkgRes) Compare(r Res) bool {
|
||||
// we can only compare PkgRes to others of the same resource kind
|
||||
res, ok := r.(*PkgRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
}
|
||||
if obj.AllowUntrusted != res.AllowUntrusted {
|
||||
return false
|
||||
}
|
||||
if obj.AllowNonFree != res.AllowNonFree {
|
||||
return false
|
||||
}
|
||||
if obj.AllowUnsupported != res.AllowUnsupported {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *PkgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes PkgRes // indirection to avoid infinite recursion
|
||||
|
||||
@@ -562,7 +652,8 @@ func (obj *PkgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReturnSvcInFileList returns a list of svc names for matches like: `/usr/lib/systemd/system/*.service`.
|
||||
// ReturnSvcInFileList returns a list of svc names for matches like:
|
||||
// `/usr/lib/systemd/system/*.service`.
|
||||
func ReturnSvcInFileList(fileList []string) []string {
|
||||
result := []string{}
|
||||
for _, x := range fileList {
|
||||
@@ -580,3 +671,10 @@ func ReturnSvcInFileList(fileList []string) []string {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// stateIsVersion is a simple test to see if the state string is an existing
|
||||
// well-known flag.
|
||||
// TODO: what should we do about the empty string?
|
||||
func stateIsVersion(state string) bool {
|
||||
return (state != PkgStateInstalled && state != PkgStateUninstalled && state != PkgStateNewest) // must be a ver. string
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,6 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
@@ -1,5 +1,5 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,139 +19,145 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("print", func() Res { return &PrintRes{} })
|
||||
engine.RegisterResource("print", func() engine.Res { return &PrintRes{} })
|
||||
}
|
||||
|
||||
// PrintRes is a resource that is useful for printing a message to the screen.
|
||||
// It will also display a message when it receives a notification. It supports
|
||||
// automatic grouping.
|
||||
type PrintRes struct {
|
||||
BaseRes `lang:"" yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Groupable
|
||||
traits.Recvable
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Msg string `lang:"msg" yaml:"msg"` // the message to display
|
||||
// RefreshOnly is an option that causes the message to be printed only
|
||||
// when notified by another resource. When set to true, this resource
|
||||
// cannot be autogrouped.
|
||||
RefreshOnly bool `lang:"refresh_only" yaml:"refresh_only"`
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *PrintRes) Default() Res {
|
||||
return &PrintRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *PrintRes) Default() engine.Res {
|
||||
return &PrintRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *PrintRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *PrintRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *PrintRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *PrintRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *PrintRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
select {
|
||||
case <-obj.init.Done: // closed by the engine to signal shutdown
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
}
|
||||
}
|
||||
//obj.init.Event() // notify engine of an event (this can block)
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckApply method for Print resource. Does nothing, returns happy!
|
||||
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply: %t", obj, apply)
|
||||
if val, exists := obj.Recv["Msg"]; exists && val.Changed {
|
||||
func (obj *PrintRes) CheckApply(apply bool) (bool, error) {
|
||||
obj.init.Logf("CheckApply: %t", apply)
|
||||
if val, exists := obj.init.Recv()["Msg"]; exists && val.Changed {
|
||||
// if we received on Msg, and it changed, log message
|
||||
log.Printf("CheckApply: Received `Msg` of: %s", obj.Msg)
|
||||
obj.init.Logf("CheckApply: Received `Msg` of: %s", obj.Msg)
|
||||
}
|
||||
|
||||
if obj.Refresh() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
var refresh = obj.init.Refresh()
|
||||
// we output if not RefreshOnly, or we are in refresh mode and RefreshOnly
|
||||
var display = refresh || !obj.RefreshOnly
|
||||
|
||||
if refresh {
|
||||
obj.init.Logf("Received a notification!")
|
||||
}
|
||||
if display {
|
||||
obj.init.Logf("Msg: %s", obj.Msg)
|
||||
}
|
||||
log.Printf("%s: Msg: %s", obj, obj.Msg)
|
||||
if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements
|
||||
for _, x := range g {
|
||||
print, ok := x.(*PrintRes) // convert from Res
|
||||
if !ok {
|
||||
log.Fatalf("grouped member %v is not a %s", x, obj.GetKind())
|
||||
panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
|
||||
}
|
||||
if display {
|
||||
obj.init.Logf("%s: Msg: %s", print, print.Msg)
|
||||
}
|
||||
log.Printf("%s: Msg: %s", print, print.Msg)
|
||||
}
|
||||
}
|
||||
return true, nil // state is always okay
|
||||
}
|
||||
|
||||
// PrintUID is the UID struct for PrintRes.
|
||||
type PrintUID struct {
|
||||
BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *PrintRes) UIDs() []ResUID {
|
||||
x := &PrintUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *PrintRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*PrintRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return true // grouped together if we were asked to
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *PrintRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PrintRes) Cmp(r engine.Res) error {
|
||||
// we can only compare PrintRes to others of the same resource kind
|
||||
res, ok := r.(*PrintRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// calling base Compare is probably unneeded for the print res, but do it
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Msg != res.Msg {
|
||||
return false
|
||||
return fmt.Errorf("the Msg differs")
|
||||
}
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
// It is primarily useful for setting the defaults.
|
||||
// PrintUID is the UID struct for PrintRes.
|
||||
type PrintUID struct {
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object. Most
|
||||
// resources only return one, although some resources can return multiple.
|
||||
func (obj *PrintRes) UIDs() []engine.ResUID {
|
||||
x := &PrintUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *PrintRes) GroupCmp(r engine.GroupableRes) error {
|
||||
res, ok := r.(*PrintRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("resource is not the same kind")
|
||||
}
|
||||
// we don't group if it's RefreshOnly: only the notifier may trigger
|
||||
if obj.RefreshOnly || res.RefreshOnly {
|
||||
return fmt.Errorf("resource uses RefreshOnly, it cannot be merged")
|
||||
}
|
||||
return nil // grouped together if we were asked to
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes PrintRes // indirection to avoid infinite recursion
|
||||
|
||||
1749
engine/resources/resources_test.go
Normal file
1749
engine/resources/resources_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user