Compare commits
1089 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5efe7a17b | ||
|
|
7075b8b973 | ||
|
|
3f5957d30e | ||
|
|
bc29957d1e | ||
|
|
289835039a | ||
|
|
b1e08ef231 | ||
|
|
8a463767bf | ||
|
|
c598e4d289 | ||
|
|
a7624a2bf9 | ||
|
|
d20fcbd845 | ||
|
|
5d664855de | ||
|
|
8366cf0873 | ||
|
|
a41789a746 | ||
|
|
cde3251dd8 | ||
|
|
7c394bf735 | ||
|
|
76e0345609 | ||
|
|
d8820fa185 | ||
|
|
b6502693e4 | ||
|
|
f7e5402966 | ||
|
|
1e6a825412 | ||
|
|
c23065aacd | ||
|
|
04f5ba67a2 | ||
|
|
b87fa6715b | ||
|
|
f6f3298e03 | ||
|
|
6bfd781947 | ||
|
|
aff6331211 | ||
|
|
d547c39a16 | ||
|
|
3cea422365 | ||
|
|
ac39606386 | ||
|
|
12ae44d563 | ||
|
|
57b37d9005 | ||
|
|
9d5cc07567 | ||
|
|
75d4d767c6 | ||
|
|
0be4b86230 | ||
|
|
784d15b012 | ||
|
|
00f6045b12 | ||
|
|
b26f842de1 | ||
|
|
0ab2406db9 | ||
|
|
bf7e45439b | ||
|
|
0652273fe1 | ||
|
|
5927a54208 | ||
|
|
b46db59948 | ||
|
|
23b5a4729f | ||
|
|
8ae47bd490 | ||
|
|
1796d20399 | ||
|
|
5ac2447b85 | ||
|
|
db445c3a8e | ||
|
|
de2914978d | ||
|
|
09812a7bfc | ||
|
|
2eb3b541f4 | ||
|
|
e9791ff92c | ||
|
|
88516546fa | ||
|
|
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 | ||
|
|
3ad7097c8a | ||
|
|
8e01b6db48 | ||
|
|
67607eba8b | ||
|
|
6e7a71d01a | ||
|
|
ff69a82b57 | ||
|
|
df1e50e599 | ||
|
|
6370f0cb95 | ||
|
|
80784bb8f1 | ||
|
|
40dcd6ec99 | ||
|
|
06f2d65500 | ||
|
|
98d3c299ff | ||
|
|
46da3a34a0 | ||
|
|
f33f84d2f2 | ||
|
|
a785a43ef3 | ||
|
|
b0911c6d70 | ||
|
|
81a0e9e8c7 | ||
|
|
06d33a45f5 | ||
|
|
cfb8deac56 | ||
|
|
9544ab2e02 | ||
|
|
318fe4a5dc | ||
|
|
5597183391 | ||
|
|
05c60d9a59 | ||
|
|
f01eea33e9 | ||
|
|
9992c367bf | ||
|
|
d275a23a81 | ||
|
|
14ddd7c196 | ||
|
|
0815b20b76 | ||
|
|
cffdb06181 | ||
|
|
837388ae4e | ||
|
|
cbd2bdd4c5 | ||
|
|
f34ca3a5ca | ||
|
|
4898297cce | ||
|
|
ffcc2aa2af | ||
|
|
158fb8d31c | ||
|
|
07714c67cb | ||
|
|
f12e502c61 | ||
|
|
2fdf8d5dc3 | ||
|
|
28ec7a1e54 | ||
|
|
24cb2e6450 | ||
|
|
915b022901 | ||
|
|
4a623c1891 | ||
|
|
7508161c39 | ||
|
|
d33861ccb4 | ||
|
|
572b2575c5 | ||
|
|
d99190b166 | ||
|
|
bc91b03276 | ||
|
|
9ba893c06c | ||
|
|
27e51f1bcb | ||
|
|
e3a26483e8 | ||
|
|
33a4fd6fbe | ||
|
|
b34b359860 | ||
|
|
8b9491823d | ||
|
|
b8b6e5266f | ||
|
|
5f80c1ac2a | ||
|
|
3af7e815d0 | ||
|
|
714afe35a1 | ||
|
|
b0a8f585c3 | ||
|
|
1a2918082d | ||
|
|
22e4dfa534 | ||
|
|
41eb850b3d | ||
|
|
3a50171d19 | ||
|
|
6c9e0ff974 | ||
|
|
644e5164b1 | ||
|
|
4fefa9f2f0 | ||
|
|
4c793e0ee6 | ||
|
|
68a7de41ae | ||
|
|
94c8bc1de9 | ||
|
|
8fb0373f82 | ||
|
|
d567dc3769 | ||
|
|
ba21554c5f | ||
|
|
e37bb3ac8a | ||
|
|
79845f0dfd | ||
|
|
eb33a5a5df | ||
|
|
adbe9c7be1 | ||
|
|
b19583e7d3 | ||
|
|
1c8c0b2915 | ||
|
|
bee1aa00f1 | ||
|
|
077b6e540a | ||
|
|
e8b03545bb | ||
|
|
70c59eab4a | ||
|
|
3c677543e0 | ||
|
|
c455ef2c62 | ||
|
|
032d0992d6 | ||
|
|
67837a47ac | ||
|
|
32e3c4e029 | ||
|
|
76fcb7a06e | ||
|
|
149a2188e2 | ||
|
|
08e7caea6b | ||
|
|
e330ebc8c9 | ||
|
|
388a08e13a | ||
|
|
9ba9ef1cbf | ||
|
|
fac004b774 | ||
|
|
8cd3f28734 | ||
|
|
dcd23fcf75 | ||
|
|
1162485c2c | ||
|
|
966172eac6 | ||
|
|
12fce52cd7 | ||
|
|
5ca1e2a23f | ||
|
|
98f8a61e83 | ||
|
|
2e86d7c5ab | ||
|
|
62ca12608d | ||
|
|
406aa55667 | ||
|
|
a76dce8b15 | ||
|
|
b01d453ae3 | ||
|
|
ac629404f4 | ||
|
|
3575d597f7 | ||
|
|
2affcba3b4 | ||
|
|
846c5f8762 | ||
|
|
086af712d2 | ||
|
|
2b6e39f283 | ||
|
|
472663193a | ||
|
|
879ff838ae | ||
|
|
5e9a085e39 | ||
|
|
c2b5729ebd | ||
|
|
fdce9d6a6a | ||
|
|
bfc2549289 | ||
|
|
52fd1ae73e | ||
|
|
23e167616f | ||
|
|
51ce83f20b | ||
|
|
5e5bbf4b39 | ||
|
|
cbc3a691b9 | ||
|
|
a5247d6e69 | ||
|
|
d698b82a83 | ||
|
|
91eff75288 | ||
|
|
91a9edb322 | ||
|
|
c8ddbeaa5c | ||
|
|
3634b3450d | ||
|
|
c2a5e3f5d8 | ||
|
|
db49fe85e4 | ||
|
|
567a2e9fd8 | ||
|
|
987de00e17 | ||
|
|
baeafec74a | ||
|
|
9cfa0b14d4 | ||
|
|
948ded6792 | ||
|
|
3c69619fd9 | ||
|
|
e7c4bc7f47 | ||
|
|
277ecc901b | ||
|
|
0f70c31a30 | ||
|
|
9a97a92e31 | ||
|
|
f9d452ad2c | ||
|
|
9907c12eda | ||
|
|
19533a32b5 | ||
|
|
c5a5004f9e | ||
|
|
677cdea99d | ||
|
|
4d7c0ddbce | ||
|
|
81daf10157 | ||
|
|
b3ef4e41bf | ||
|
|
9fbf149717 | ||
|
|
95cb94a039 | ||
|
|
21f7f87716 | ||
|
|
831c7e2c32 | ||
|
|
cc0d04c8b7 | ||
|
|
46be83f8f7 | ||
|
|
28560e2045 | ||
|
|
0df4824a56 | ||
|
|
dbcabc6517 | ||
|
|
69f479b67e | ||
|
|
af75696018 | ||
|
|
80b8f8740f | ||
|
|
71ab325940 | ||
|
|
653c76709a | ||
|
|
83cc1bab38 | ||
|
|
6c8588c019 | ||
|
|
5b00ed2fb2 | ||
|
|
9f66962bfb | ||
|
|
0edba74091 | ||
|
|
1003b49dd9 | ||
|
|
884ba54f96 | ||
|
|
cf2325a2da | ||
|
|
db6972638d | ||
|
|
74e04e81d5 | ||
|
|
7c5d7365c7 | ||
|
|
0dadf3d78a | ||
|
|
e341256627 | ||
|
|
5a3bd3ca67 | ||
|
|
8102e0a468 | ||
|
|
7d55179727 | ||
|
|
bc1a1d1818 | ||
|
|
a8bbb22fe8 | ||
|
|
6b489f71a1 | ||
|
|
f1db088af4 | ||
|
|
6fe12b3fb5 | ||
|
|
dacbf9b68d | ||
|
|
9f5057eac7 | ||
|
|
525cd54921 | ||
|
|
7ac94bbf5f | ||
|
|
b8ff6938df | ||
|
|
2f6c77fba2 | ||
|
|
28a6430778 | ||
|
|
6e4157da35 | ||
|
|
4f420dde05 | ||
|
|
d9601471df | ||
|
|
9941a97e37 | ||
|
|
0a64b08669 | ||
|
|
4d9d0d4548 | ||
|
|
5f6c8545c6 | ||
|
|
ddc335d65a | ||
|
|
9cbaa892d3 | ||
|
|
9531465410 | ||
|
|
c35916fad1 | ||
|
|
bf476a058e | ||
|
|
d4e815a4cb | ||
|
|
0545c4167b | ||
|
|
6838dd02c0 | ||
|
|
14c2fd1edd | ||
|
|
6e503cc79b | ||
|
|
bd4563b699 | ||
|
|
458e115490 | ||
|
|
51369adad1 | ||
|
|
f65c5fb147 | ||
|
|
4150ae7307 | ||
|
|
a87288d519 | ||
|
|
3cf9639e99 | ||
|
|
4490c3ed1a | ||
|
|
fbcb562781 | ||
|
|
b1e035f96a | ||
|
|
11c3a26c23 | ||
|
|
1fbe72b52d | ||
|
|
f4bb066737 | ||
|
|
aaac9cbeeb | ||
|
|
0e68ff6923 | ||
|
|
1c59712cbf | ||
|
|
c2cb1c9168 | ||
|
|
cc8e2e40dd | ||
|
|
e67d97d9da | ||
|
|
d74c2115fd | ||
|
|
70e7ee2d46 | ||
|
|
d11854f4e8 | ||
|
|
4bb553e015 | ||
|
|
0af9af44e5 | ||
|
|
3a0d73f740 | ||
|
|
9b9ff2622d | ||
|
|
a4858be967 | ||
|
|
6fd5623b1f | ||
|
|
66d9c7091c | ||
|
|
525a1e8140 | ||
|
|
64dc47d7e9 | ||
|
|
f3fc7bb91e | ||
|
|
028ef14cc0 | ||
|
|
3e001f9a1c | ||
|
|
33d20ac6d8 | ||
|
|
660554cc45 | ||
|
|
a455324e8c | ||
|
|
cd5e2e1148 | ||
|
|
074da4da19 | ||
|
|
e4e39d820c | ||
|
|
e5dbb214a2 | ||
|
|
91af528ff8 | ||
|
|
18c4e39ea3 | ||
|
|
bda455ce78 | ||
|
|
a07aea1ad3 | ||
|
|
18e2dbf144 | ||
|
|
564a07e62e | ||
|
|
a358135e41 | ||
|
|
6d9be15035 | ||
|
|
b740e0b78a | ||
|
|
9546949945 | ||
|
|
8ff048d055 | ||
|
|
95a1c6e7fb | ||
|
|
0b1a4a0f30 | ||
|
|
22b48e296a | ||
|
|
c696ebf53c | ||
|
|
a0686b7d2b | ||
|
|
8d94be8924 | ||
|
|
e97ac5033f | ||
|
|
44771a0049 | ||
|
|
32aae8f57a | ||
|
|
8207e23cd9 | ||
|
|
a469029698 | ||
|
|
203d866643 | ||
|
|
1488e5ec4d | ||
|
|
af66138a17 | ||
|
|
5f060d60a7 | ||
|
|
73ccbb69ea | ||
|
|
be60440b20 | ||
|
|
837efb78e6 | ||
|
|
4a62a290d8 | ||
|
|
018399cb1f | ||
|
|
646a576358 | ||
|
|
d8e19cd79a | ||
|
|
757cb0cf23 | ||
|
|
7d92ab335a | ||
|
|
46c6d6f656 | ||
|
|
46260749c1 | ||
|
|
50664fe115 | ||
|
|
c480bd94db | ||
|
|
79923a939b | ||
|
|
327b22113a | ||
|
|
12160ab539 | ||
|
|
2462ea0892 | ||
|
|
8be09eadd4 | ||
|
|
98bc96c911 | ||
|
|
b0fce6a80d | ||
|
|
53b8a21d1e | ||
|
|
1346492d72 | ||
|
|
e5bb8d7992 | ||
|
|
49594b8435 | ||
|
|
3bd37a7906 | ||
|
|
e070a85ae0 | ||
|
|
c189278e24 | ||
|
|
2a8606bd98 | ||
|
|
18ea05c837 | ||
|
|
86c3072515 | ||
|
|
fccf508dde | ||
|
|
2da21f90f4 | ||
|
|
bec7f1726f | ||
|
|
74dfb9d88d | ||
|
|
02dddfc227 | ||
|
|
545016b38f | ||
|
|
0ccceaf226 | ||
|
|
a601115650 | ||
|
|
ae6267c906 | ||
|
|
ac142694f5 | ||
|
|
69b0913315 | ||
|
|
421bacd7dc | ||
|
|
573a76eedb | ||
|
|
b7948c7f40 | ||
|
|
2647d09b8f | ||
|
|
57e919d7e5 | ||
|
|
f456aa1407 | ||
|
|
d0d62892c8 | ||
|
|
a981cfa053 | ||
|
|
55290dd1e3 | ||
|
|
9c4e255994 | ||
|
|
f9c7d5f7bc | ||
|
|
49baea5f6a | ||
|
|
6209cf3933 | ||
|
|
d170a523c3 | ||
|
|
be5040e7a8 | ||
|
|
ecbaa5bfc1 | ||
|
|
25e2af7c89 | ||
|
|
605688426d | ||
|
|
0e069f1e75 | ||
|
|
e9adbf18d3 | ||
|
|
610202097a | ||
|
|
8c2c552164 | ||
|
|
b9976cf693 | ||
|
|
3261c405bd | ||
|
|
35d3328e3e | ||
|
|
e96041d76f | ||
|
|
c2034bc0c0 | ||
|
|
e8855f7621 | ||
|
|
bdb8368e89 | ||
|
|
f160db2032 | ||
|
|
de9a32a273 | ||
|
|
6ba7422c3b | ||
|
|
5cbb0ceb80 | ||
|
|
5b29358b37 | ||
|
|
90147f3dfb | ||
|
|
72873abe05 | ||
|
|
de1810ba68 | ||
|
|
7b7c765d78 | ||
|
|
806d4660cf | ||
|
|
5ae5d364bb | ||
|
|
1af67e72d4 | ||
|
|
ed268ad683 | ||
|
|
5bdd2ca02f | ||
|
|
eb59861d4d | ||
|
|
427e46a2aa | ||
|
|
68a8649292 | ||
|
|
2aff8709a5 | ||
|
|
62c3add888 | ||
|
|
3ac878db62 | ||
|
|
c247cd8fea | ||
|
|
b6772b7280 | ||
|
|
807a3df9d1 | ||
|
|
491d60e267 | ||
|
|
4811eafd67 | ||
|
|
8dedbb9620 | ||
|
|
dd8454161f | ||
|
|
9421f2cddd | ||
|
|
d8c4f78ec1 | ||
|
|
54296da647 | ||
|
|
357102fdb5 | ||
|
|
7e15a9e181 | ||
|
|
12e0b2d6f7 | ||
|
|
11b40bf32f | ||
|
|
8d2b53373f | ||
|
|
9ecc49e592 | ||
|
|
4f34f7083b | ||
|
|
2a6df875ec | ||
|
|
51c83116a2 | ||
|
|
74435aac76 | ||
|
|
5dfdb5b5f9 | ||
|
|
ac892a3f3d | ||
|
|
1a2e99f559 | ||
|
|
e97bba0524 | ||
|
|
0538f0c524 | ||
|
|
fc3e35868d | ||
|
|
f1e0cfea1c | ||
|
|
56efef69ba | ||
|
|
668ec8a248 | ||
|
|
60912bd01c | ||
|
|
0b416e44f8 | ||
|
|
ecc4aa09d3 | ||
|
|
b921aabbed | ||
|
|
6ad8ac0b6b | ||
|
|
44e7e0e970 | ||
|
|
45820b4ce3 | ||
|
|
3a098377cb | ||
|
|
35875485ee | ||
|
|
19760be0bc | ||
|
|
b3ea33f88d | ||
|
|
5b3425a689 | ||
|
|
a3d157bde6 | ||
|
|
2c8c9264a4 | ||
|
|
0009d9b20e | ||
|
|
dd8d17232f | ||
|
|
6312b9225f | ||
|
|
68cc09fef2 | ||
|
|
0651c9de65 | ||
|
|
38261ec809 | ||
|
|
067932aebf | ||
|
|
af47511d58 | ||
|
|
36b916f27f | ||
|
|
e519811893 |
2
.ackrc
2
.ackrc
@@ -1,3 +1,5 @@
|
||||
--ignore-dir=old/
|
||||
--ignore-dir=tmp/
|
||||
--ignore-dir=vendor/
|
||||
--ignore-dir=releases/
|
||||
--ignore-dir=rpmbuild/
|
||||
|
||||
@@ -12,8 +12,14 @@ end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.sh]
|
||||
indent_style = tab
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.mcl]
|
||||
indent_style = tab
|
||||
|
||||
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
|
||||
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
## Versions:
|
||||
|
||||
* mgmt version (eg: `mgmt --version`):
|
||||
|
||||
* operating system/distribution (eg: `uname -a`):
|
||||
|
||||
* golang version (eg: `go version`):
|
||||
|
||||
## Description:
|
||||
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
47
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
## Tips:
|
||||
|
||||
* please read the style guide before submitting your patch:
|
||||
[docs/style-guide.md](../docs/style-guide.md)
|
||||
|
||||
* commit message titles must be in the form:
|
||||
|
||||
```topic: Capitalized message with no trailing period```
|
||||
|
||||
or:
|
||||
|
||||
```topic, topic2: Capitalized message with no trailing period```
|
||||
|
||||
* golang code must be formatted according to the standard, please run:
|
||||
|
||||
```
|
||||
make gofmt # formats the entire project correctly
|
||||
```
|
||||
|
||||
or format a single golang file correctly:
|
||||
|
||||
```
|
||||
gofmt -w yourcode.go
|
||||
```
|
||||
|
||||
* please rebase your patch against current git master:
|
||||
|
||||
```
|
||||
git checkout master
|
||||
git pull origin master
|
||||
git checkout your-feature
|
||||
git rebase master
|
||||
git push your-remote your-feature
|
||||
hub pull-request # or submit with the github web ui
|
||||
```
|
||||
|
||||
* after a patch review, please ping @purpleidea so we know to re-review:
|
||||
|
||||
```
|
||||
# make changes based on reviews...
|
||||
git add -p # add new changes
|
||||
git commit --amend # combine with existing commit
|
||||
git push your-remote your-feature -f
|
||||
# now ping @purpleidea in the github PR since it doesn't notify us automatically
|
||||
```
|
||||
|
||||
## Thanks for contributing to mgmt and welcome to the team!
|
||||
98
.github/settings.yml
vendored
Normal file
98
.github/settings.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
|
||||
|
||||
repository:
|
||||
# See https://developer.github.com/v3/repos/#edit for all available settings.
|
||||
|
||||
# The name of the repository. Changing this will rename the repository
|
||||
name: mgmt
|
||||
|
||||
# A short description of the repository that will show up on GitHub
|
||||
description: Next generation distributed, event-driven, parallel config management!
|
||||
|
||||
# A URL with more information about the repository
|
||||
homepage: https://purpleidea.com/tags/mgmtconfig/
|
||||
|
||||
# A comma-separated list of topics to set on the repository
|
||||
topics: golang, go, configuration-management, config-management, devops, etcd, distributed-systems, graph-theory, choreography
|
||||
|
||||
# Either `true` to make the repository private, or `false` to make it public.
|
||||
private: false
|
||||
|
||||
# Either `true` to enable issues for this repository, `false` to disable them.
|
||||
has_issues: true
|
||||
|
||||
# Either `true` to enable projects for this repository, or `false` to disable them.
|
||||
# If projects are disabled for the organization, passing `true` will cause an API error.
|
||||
has_projects: false
|
||||
|
||||
# Either `true` to enable the wiki for this repository, `false` to disable it.
|
||||
has_wiki: false
|
||||
|
||||
# Either `true` to enable downloads for this repository, `false` to disable them.
|
||||
has_downloads: true
|
||||
|
||||
# Updates the default branch for this repository.
|
||||
default_branch: master
|
||||
|
||||
# Either `true` to allow squash-merging pull requests, or `false` to prevent
|
||||
# squash-merging.
|
||||
allow_squash_merge: false
|
||||
|
||||
# Either `true` to allow merging pull requests with a merge commit, or `false`
|
||||
# to prevent merging pull requests with merge commits.
|
||||
allow_merge_commit: false
|
||||
|
||||
# Either `true` to allow rebase-merging pull requests, or `false` to prevent
|
||||
# rebase-merging.
|
||||
allow_rebase_merge: true
|
||||
|
||||
# Labels: define labels for Issues and Pull Requests (in alphabetical order)
|
||||
labels:
|
||||
- name: bug
|
||||
color: fc2929
|
||||
- name: confirmed
|
||||
color: d93f0b
|
||||
- name: design
|
||||
color: 5319e7
|
||||
- name: duplicate
|
||||
color: cccccc
|
||||
- name: enhancement
|
||||
color: 84b6eb
|
||||
- name: good first issue
|
||||
color: 7057ff
|
||||
- name: help wanted
|
||||
color: 159818
|
||||
- name: invalid
|
||||
color: e6e6e6
|
||||
- name: mgmtlove
|
||||
color: e11d21
|
||||
- name: question
|
||||
color: cc317c
|
||||
- name: needinfo
|
||||
color: fbca04
|
||||
- name: wontfix
|
||||
color: ffffff
|
||||
# - name: first-timers-only
|
||||
# # include the old name to rename an existing label
|
||||
# oldname: Help Wanted
|
||||
|
||||
# Collaborators: give specific users access to this repository.
|
||||
#collaborators:
|
||||
# - username: purpleidea
|
||||
# # Note: Only valid on organization-owned repositories.
|
||||
# # The permission to grant the collaborator. Can be one of:
|
||||
# # * `pull` - can pull, but not push to or administer this repository.
|
||||
# # * `push` - can pull and push, but not administer this repository.
|
||||
# # * `admin` - can pull, push and administer this repository.
|
||||
# permission: push
|
||||
|
||||
# - username: hubot
|
||||
# permission: pull
|
||||
|
||||
# NOTE: The APIs needed for teams are not supported yet by GitHub Apps
|
||||
# https://developer.github.com/v3/apps/available-endpoints/
|
||||
#teams:
|
||||
# - name: core
|
||||
# permission: admin
|
||||
# - name: docs
|
||||
# permission: push
|
||||
68
.github/workflows/test.yaml
vendored
Normal file
68
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
# 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.19.x and tip
|
||||
# minimum required and latest published go_version
|
||||
- 1.18
|
||||
test_block:
|
||||
- basic
|
||||
- shell
|
||||
- race
|
||||
#fail-fast: false
|
||||
|
||||
steps:
|
||||
# Do not shallow fetch. 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
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -2,11 +2,19 @@
|
||||
.omv/
|
||||
.ssh/
|
||||
.vagrant/
|
||||
mgmt-documentation.pdf
|
||||
.envrc
|
||||
old/
|
||||
tmp/
|
||||
*WIP
|
||||
*_stringer.go
|
||||
mgmt
|
||||
mgmt.static
|
||||
# crossbuild artifacts
|
||||
build/mgmt-*
|
||||
mgmt.iml
|
||||
rpmbuild/
|
||||
releases/
|
||||
# vim swap files
|
||||
.*.sw[op]
|
||||
# prevent `echo foo 2>1` typo errors by making this file read-only
|
||||
1
|
||||
|
||||
15
.gitmodules
vendored
15
.gitmodules
vendored
@@ -1,15 +0,0 @@
|
||||
[submodule "vendor/github.com/coreos/etcd"]
|
||||
path = vendor/github.com/coreos/etcd
|
||||
url = https://github.com/coreos/etcd/
|
||||
[submodule "vendor/google.golang.org/grpc"]
|
||||
path = vendor/google.golang.org/grpc
|
||||
url = https://github.com/grpc/grpc-go
|
||||
[submodule "vendor/github.com/grpc-ecosystem/grpc-gateway"]
|
||||
path = vendor/github.com/grpc-ecosystem/grpc-gateway
|
||||
url = https://github.com/grpc-ecosystem/grpc-gateway
|
||||
[submodule "vendor/gopkg.in/fsnotify.v1"]
|
||||
path = vendor/gopkg.in/fsnotify.v1
|
||||
url = https://gopkg.in/fsnotify.v1
|
||||
[submodule "vendor/github.com/purpleidea/go-systemd"]
|
||||
path = vendor/github.com/purpleidea/go-systemd
|
||||
url = https://github.com/purpleidea/go-systemd
|
||||
61
.travis.yml
61
.travis.yml
@@ -1,22 +1,54 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
os:
|
||||
- linux
|
||||
go_import_path: github.com/purpleidea/mgmt
|
||||
sudo: true
|
||||
dist: trusty
|
||||
before_install: 'git fetch --unshallow'
|
||||
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
|
||||
# https://docs.travis-ci.com/user/common-build-problems/#travis_retry
|
||||
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then travis_retry travis_retry sudo apt update; fi
|
||||
- git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
|
||||
- git fetch --unshallow
|
||||
install: 'make deps'
|
||||
script: 'make test'
|
||||
matrix:
|
||||
fast_finish: true
|
||||
fast_finish: false
|
||||
allow_failures:
|
||||
- go: tip
|
||||
- go: 1.7
|
||||
- go: 1.19.x
|
||||
- go: tip
|
||||
- os: osx
|
||||
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
||||
include:
|
||||
- name: "basic tests"
|
||||
go: 1.18.x
|
||||
env: TEST_BLOCK=basic
|
||||
- name: "shell tests"
|
||||
go: 1.18.x
|
||||
env: TEST_BLOCK=shell
|
||||
- name: "race tests"
|
||||
go: 1.18.x
|
||||
env: TEST_BLOCK=race
|
||||
- go: 1.19.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:
|
||||
- "irc.freenode.net#mgmtconfig"
|
||||
#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}"
|
||||
@@ -25,4 +57,7 @@ notifications:
|
||||
use_notice: false
|
||||
skip_join: false
|
||||
email:
|
||||
- travis-ci@shubin.ca
|
||||
recipients:
|
||||
- secure: qNkgP6QLl6VXpFQIxas2wggxvIiOmm1/hGRXm4BXsSFzHsJPvMamA3E1HEC7H+luiWTny1jtGSGgTJPV9CX1LtQV0g0S4ThaAvWuKvk3rXO8IVd++iA/Lh1s1H6JdKM0dJtLqFICawjeci4tOQzSvrM2eCBWqT0UYsrQsGHB6AF31GNAH0Acqd5cYeL+ZpbCN+hQEznAZQ7546N25TwqieI8Lg7nisA+lwYYwsaC2+f5RIeyvvKjQv3wzEdBAQ9CI9WQiTOUBnUnyYxMrdomQ/XGF66QnZy9vq5nEP83IFtuhPvSamL7ceT+yJW0jDyBi8sYEV7On7eXzjyHbiYpF4YHcJrFnf5RyV4kQGd6/SC8iZwK4Is4eyeAjDFTC+JafLajw9R9x9bK43BwlRAWOZxjFKe0cU/BVAjmlz87vHgUho2P41+0a5XfajfU6VhA5QFPK6rNH7W1CnA7D/0LmS0yaqJM1OCrm6LfoZEMhe0DxTJ9uWJbr0x1sYao6q8H4xYk+fyRgoBAr2TxYU7kXx8ThiRdzuQ8izdbojlzTYLe8liZMIsjL0axLsLK7YBWrjJUcDFDjR/DqmVxPrvbVFbCi9ChmBw0WmbJvDY0FV8T8dO8wCjg9JEmprAmWPyq0g/F87LFK4tAZqQFJGjP1qwsR9jdwdNTKeCdY656f/Y=
|
||||
on_failure: change
|
||||
on_success: change
|
||||
|
||||
7
AUTHORS
7
AUTHORS
@@ -1,8 +1,13 @@
|
||||
This is a list of authors/contributors to the mgmt project.
|
||||
If you're a contributor, please send a patch with your name.
|
||||
If you're a core contributor, we might ask you to send a patch with your name.
|
||||
If you appreciate the work of one of the contributors, thank them a beverage!
|
||||
For a more exhaustive list please run: git log --format='%aN' | sort -u
|
||||
This list is sorted alphabetically by first name.
|
||||
|
||||
Felix Frank
|
||||
James Shubin
|
||||
Joe Groocock
|
||||
Johan Bloemberg
|
||||
Jonathan Gold
|
||||
Julien Pivotto
|
||||
Paul Morgan
|
||||
|
||||
141
COPYING
141
COPYING
@@ -1,5 +1,5 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
@@ -7,15 +7,17 @@
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -60,7 +72,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
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 Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
Mgmt
|
||||
Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
Copyright (C) 2013-2023+ 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 Affero General Public License as published by
|
||||
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 Affero General Public License for more details.
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
564
DOCUMENTATION.md
564
DOCUMENTATION.md
@@ -1,564 +0,0 @@
|
||||
#mgmt
|
||||
|
||||
<!--
|
||||
Mgmt
|
||||
Copyright (C) 2013-2016+ 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 Affero 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 Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
##mgmt by [James](https://ttboj.wordpress.com/)
|
||||
####Available from:
|
||||
####[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/)
|
||||
|
||||
####This documentation is available in: [Markdown](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md) format.
|
||||
|
||||
####Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Project description - What the project does](#project-description)
|
||||
3. [Setup - Getting started with mgmt](#setup)
|
||||
4. [Features - All things mgmt can do](#features)
|
||||
* [Autoedges - Automatic resource relationships](#autoedges)
|
||||
* [Autogrouping - Automatic resource grouping](#autogrouping)
|
||||
* [Automatic clustering - Automatic cluster management](#automatic-clustering)
|
||||
* [Remote mode - Remote "agent-less" execution](#remote-agent-less-mode)
|
||||
* [Puppet support - write manifest code for mgmt](#puppet-support)
|
||||
5. [Resources - All built-in primitives](#resources)
|
||||
6. [Usage/FAQ - Notes on usage and frequently asked questions](#usage-and-frequently-asked-questions)
|
||||
7. [Reference - Detailed reference](#reference)
|
||||
* [Meta parameters](#meta-parameters)
|
||||
* [Graph definition file](#graph-definition-file)
|
||||
* [Command line](#command-line)
|
||||
8. [Examples - Example configurations](#examples)
|
||||
9. [Development - Background on module development and reporting bugs](#development)
|
||||
10. [Authors - Authors and contact information](#authors)
|
||||
|
||||
##Overview
|
||||
|
||||
The `mgmt` tool is a next generation config management prototype. It's not yet
|
||||
ready for production, but we hope to get there soon. Get involved today!
|
||||
|
||||
##Project Description
|
||||
|
||||
The mgmt tool is a distributed, event driven, config management tool, that
|
||||
supports parallel execution, and librarification to be used as the management
|
||||
foundation in and for, new and existing software.
|
||||
|
||||
For more information, you may like to read some blog posts from the author:
|
||||
|
||||
* [Next generation config mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* [Automatic edges in mgmt](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
* [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
* [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
|
||||
There is also an [introductory video](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) available.
|
||||
Older videos and other material [is available](https://github.com/purpleidea/mgmt/#on-the-web).
|
||||
|
||||
##Setup
|
||||
|
||||
During this prototype phase, the tool can be run out of the source directory.
|
||||
You'll probably want to use ```./run.sh run --yaml examples/graph1.yaml``` to
|
||||
get started. Beware that this _can_ cause data loss. Understand what you're
|
||||
doing first, or perform these actions in a virtual environment such as the one
|
||||
provided by [Oh-My-Vagrant](https://github.com/purpleidea/oh-my-vagrant).
|
||||
|
||||
##Features
|
||||
|
||||
This section details the numerous features of mgmt and some caveats you might
|
||||
need to be aware of.
|
||||
|
||||
###Autoedges
|
||||
|
||||
Automatic edges, or AutoEdges, is the mechanism in mgmt by which it will
|
||||
automatically create dependencies for you between resources. For example,
|
||||
since mgmt can discover which files are installed by a package it will
|
||||
automatically ensure that any file resource you declare that matches a
|
||||
file installed by your package resource will only be processed after the
|
||||
package is installed.
|
||||
|
||||
####Controlling autoedges
|
||||
|
||||
Though autoedges is likely to be very helpful and avoid you having to declare
|
||||
all dependencies explicitly, there are cases where this behaviour is
|
||||
undesirable.
|
||||
|
||||
Some distributions allow package installations to automatically start the
|
||||
service they ship. This can be problematic in the case of packages like MySQL
|
||||
as there are configuration options that need to be set before MySQL is ever
|
||||
started for the first time (or you'll need to wipe the data directory). In
|
||||
order to handle this situation you can disable autoedges per resource and
|
||||
explicitly declare that you want `my.cnf` to be written to disk before the
|
||||
installation of the `mysql-server` package.
|
||||
|
||||
You can disable autoedges for a resource by setting the `autoedge` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
|
||||
###Autogrouping
|
||||
|
||||
Automatic grouping or AutoGroup is the mechanism in mgmt by which it will
|
||||
automatically group multiple resource vertices into a single one. This is
|
||||
particularly useful for grouping multiple package resources into a single
|
||||
resource, since the multiple installations can happen together in a single
|
||||
transaction, which saves a lot of time because package resources typically have
|
||||
a large fixed cost to running (downloading and verifying the package repo) and
|
||||
if they are grouped they share this fixed cost. This grouping feature can be
|
||||
used for other use cases too.
|
||||
|
||||
You can disable autogrouping for a resource by setting the `autogroup` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
|
||||
###Automatic clustering
|
||||
|
||||
Automatic clustering is a feature by which mgmt automatically builds, scales,
|
||||
and manages the embedded etcd cluster which is compiled into mgmt itself. It is
|
||||
quite helpful for rapidly bootstrapping clusters and avoiding the extra work to
|
||||
setup etcd.
|
||||
|
||||
If you prefer to avoid this feature. you can always opt to use an existing etcd
|
||||
cluster that is managed separately from mgmt by pointing your mgmt agents at it
|
||||
with the `--seeds` variable.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
|
||||
###Remote ("agent-less") mode
|
||||
|
||||
Remote mode is a special mode that lets you kick off mgmt runs on one or more
|
||||
remote machines which are only accessible via SSH. In this mode the initiating
|
||||
host connects over SSH, copies over the `mgmt` binary, opens an SSH tunnel, and
|
||||
runs the remote program while simultaneously passing the etcd traffic back
|
||||
through the tunnel so that the initiators etcd cluster can be used to exchange
|
||||
resource data.
|
||||
|
||||
The interesting benefit of this architecture is that multiple hosts which can't
|
||||
connect directly use the initiator to pass the important traffic through to each
|
||||
other. Once the cluster has converged all the remote programs can shutdown
|
||||
leaving no residual agent.
|
||||
|
||||
This mode can also be useful for bootstrapping a new host where you'd like to
|
||||
have the service run continuously and as part of an mgmt cluster normally.
|
||||
|
||||
In particular, when combined with the `--converged-timeout` parameter, the
|
||||
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.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/)
|
||||
|
||||
###Puppet support
|
||||
|
||||
You can supply a Puppet manifest instead of creating the (YAML) graph manually.
|
||||
Puppet must be installed and in `mgmt`'s search path. You also need the
|
||||
[ffrank-mgmtgraph Puppet module](https://forge.puppet.com/ffrank/mgmtgraph).
|
||||
|
||||
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
|
||||
|
||||
2. Compile a local manifest file (like `puppet apply`)
|
||||
|
||||
mgmt run --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 }'
|
||||
|
||||
For more details and caveats see [Puppet.md](Puppet.md).
|
||||
|
||||
####Blog post
|
||||
|
||||
An introductory post on the Puppet support is on
|
||||
[Felix's blog](http://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/).
|
||||
|
||||
##Resources
|
||||
|
||||
This section lists all the built-in resources and their properties. The
|
||||
resource primitives in `mgmt` are typically more powerful than resources in
|
||||
other configuration management systems because they can be event based which
|
||||
lets them respond in real-time to converge to the desired state. This property
|
||||
allows you to build more complex resources that you probably hadn't considered
|
||||
in the past.
|
||||
|
||||
In addition to the resource specific properties, there are resource properties
|
||||
(otherwise known as parameters) which can apply to every resource. These are
|
||||
called [meta parameters](#meta-parameters) and are listed 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.
|
||||
|
||||
* [Exec](#Exec): Execute shell commands on the system.
|
||||
* [File](#File): Manage files and directories.
|
||||
* [Hostname](#Hostname): Manages the hostname on the system.
|
||||
* [Msg](#Msg): Send log messages.
|
||||
* [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.
|
||||
* [Svc](#Svc): Manage system systemd services.
|
||||
* [Timer](#Timer): Manage system systemd services.
|
||||
* [Virt](#Virt): Manage virtual machines with libvirt.
|
||||
|
||||
###Exec
|
||||
|
||||
The exec resource can execute commands on your system.
|
||||
|
||||
###File
|
||||
|
||||
The file resource manages files and directories. In `mgmt`, directories are
|
||||
identified by a trailing slash in their path name. File have no such slash.
|
||||
|
||||
####Path
|
||||
|
||||
The path property specifies the file or directory that we are managing.
|
||||
|
||||
####Content
|
||||
|
||||
The content property is a string that specifies the desired file contents.
|
||||
|
||||
####Source
|
||||
|
||||
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
|
||||
|
||||
The state property describes the action we'd like to apply for the resource. The
|
||||
possible values are: `exists` and `absent`.
|
||||
|
||||
####Recurse
|
||||
|
||||
The recurse property limits whether file resource operations should recurse into
|
||||
and monitor directory contents with a depth greater than one.
|
||||
|
||||
####Force
|
||||
|
||||
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.
|
||||
|
||||
###Hostname
|
||||
|
||||
The hostname resource manages static, transient/dynamic and pretty hostnames
|
||||
on the system and watches them for changes.
|
||||
|
||||
#### static_hostname
|
||||
The static hostname 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.
|
||||
|
||||
#### transient_hostname
|
||||
The transient / dynamic hostname 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.
|
||||
|
||||
#### pretty_hostname
|
||||
The pretty hostname is a free-form UTF8 host name for presentation to the user.
|
||||
|
||||
#### hostname
|
||||
Hostname is the fallback value for all 3 fields above, if only `hostname` is
|
||||
specified, it will set all 3 fields to this value.
|
||||
|
||||
###Msg
|
||||
|
||||
The msg resource sends messages to the main log, or an external service such
|
||||
as systemd's journal.
|
||||
|
||||
###Noop
|
||||
|
||||
The noop resource does absolutely nothing. It does have some utility in testing
|
||||
`mgmt` and also as a placeholder in the resource graph.
|
||||
|
||||
###Nspawn
|
||||
|
||||
The nspawn resource is used to manage systemd-machined style containers.
|
||||
|
||||
###Password
|
||||
|
||||
The password resource can generate a random string to be used as a password. It
|
||||
will re-generate the password if it receives a refresh notification.
|
||||
|
||||
###Pkg
|
||||
|
||||
The pkg resource is used to manage system packages. This resource works on many
|
||||
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.
|
||||
|
||||
###Svc
|
||||
|
||||
The service resource is still very WIP. Please help us my improving it!
|
||||
|
||||
###Timer
|
||||
|
||||
This resource needs better documentation. Please help us my improving it!
|
||||
|
||||
###Virt
|
||||
|
||||
The virt resource can manage virtual machines via libvirt.
|
||||
|
||||
##Usage and frequently asked questions
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
|
||||
###Why did you start this project?
|
||||
|
||||
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 use etcd? What about consul?
|
||||
|
||||
Etcd and consul are both written in golang, which made them the top two
|
||||
contenders for my prototype. Ultimately a choice had to be made, and etcd was
|
||||
chosen, but it was also somewhat arbitrary. If there is available interest,
|
||||
good reasoning, *and* patches, then we would consider either switching or
|
||||
supporting both, but this is not a high priority at this time.
|
||||
|
||||
###Can I use an existing etcd cluster instead of the automatic embedded servers?
|
||||
|
||||
Yes, it's possible to use an existing etcd cluster instead of the automatic,
|
||||
elastic embedded etcd servers. To do so, simply point to the cluster with the
|
||||
`--seeds` variable, the same way you would if you were seeding a new member to
|
||||
an existing mgmt cluster.
|
||||
|
||||
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.
|
||||
|
||||
###What does the error message about an inconsistent dataDir mean?
|
||||
|
||||
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.
|
||||
```
|
||||
|
||||
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/`.
|
||||
|
||||
###Why do resources have both a `Compare` method and an `IFF` (on the UID) method?
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
###Did you know that there is a band named `MGMT`?
|
||||
|
||||
I didn't realize this when naming the project, and it is accidental. After much
|
||||
anguishing, I chose the name because it was short and I thought it was
|
||||
appropriately descriptive. If you need a less ambiguous search term or phrase,
|
||||
you can try using `mgmtconfig` or `mgmt config`.
|
||||
|
||||
###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://ttboj.wordpress.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!
|
||||
|
||||
##Reference
|
||||
Please note that there are a number of undocumented options. For more
|
||||
information on these options, please view the source at:
|
||||
[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/).
|
||||
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.
|
||||
* [Graph definition file](#graph-definition-file): Main graph definition file.
|
||||
* [Command line](#command-line): Command line parameters.
|
||||
|
||||
###Meta parameters
|
||||
These meta parameters are special parameters (or properties) which can apply to
|
||||
any resource. The usefulness of doing so will depend on the particular meta
|
||||
parameter and resource combination.
|
||||
|
||||
####AutoEdge
|
||||
Boolean. Should we generate auto edges for this resource?
|
||||
|
||||
####AutoGroup
|
||||
Boolean. Should we attempt to automatically group this resource with others?
|
||||
|
||||
####Noop
|
||||
Boolean. Should the Apply portion of the CheckApply method of the resource
|
||||
make any changes? Noop is a concatenation of no-operation.
|
||||
|
||||
####Retry
|
||||
Integer. The number of times to retry running the resource on error. Use -1 for
|
||||
infinite. This currently applies for both the Watch operation (which can fail)
|
||||
and for the CheckApply operation. While they could have separate values, 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.
|
||||
|
||||
####Delay
|
||||
Integer. Number of milliseconds to wait between retries. The same value is
|
||||
shared between the Watch and CheckApply retries. This currently applies for both
|
||||
the Watch operation (which can fail) and for the CheckApply operation. While
|
||||
they could have separate values, 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.
|
||||
|
||||
###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.
|
||||
|
||||
###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.
|
||||
|
||||
####`--max-runtime <seconds>`
|
||||
Exit when the agent has run for approximately this many seconds. This is not
|
||||
generally recommended, but may be useful for users who know what they're doing.
|
||||
|
||||
####`--noop`
|
||||
Globally force all resources into no-op mode. This also disables the export to
|
||||
etcd functionality, but does not disable resource collection, however all
|
||||
resources that are collected will have their individual noop settings set.
|
||||
|
||||
####`--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
|
||||
method that works.
|
||||
|
||||
####`--ssh-priv-id-rsa`
|
||||
Specify the path for finding SSH keys. This defaults to `~/.ssh/id_rsa`. To
|
||||
never use this method of authentication, set this to the empty string.
|
||||
|
||||
####`--cconns`
|
||||
The maximum number of concurrent remote ssh connections to run. This defaults
|
||||
to `0`, which means unlimited.
|
||||
|
||||
####`--no-caching`
|
||||
Don't allow remote caching of the remote execution binary. This will require
|
||||
the binary to be copied over for every remote execution, but it limits the
|
||||
likelihood that there is leftover information from the configuration process.
|
||||
|
||||
####`--prefix <path>`
|
||||
Specify a path to a custom working directory prefix. This directory will get
|
||||
created if it does not exist. This usually defaults to `/var/lib/mgmt/`. This
|
||||
can't be combined with the `--tmp-prefix` option. It can be combined with the
|
||||
`--allow-tmp-prefix` option.
|
||||
|
||||
####`--tmp-prefix`
|
||||
If this option is specified, a temporary prefix will be used instead of the
|
||||
default prefix. This can't be combined with the `--prefix` option.
|
||||
|
||||
####`--allow-tmp-prefix`
|
||||
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
|
||||
no binary available continue working in a temporary directory to avoid failure.
|
||||
|
||||
##Examples
|
||||
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git
|
||||
source repository. It is available from:
|
||||
|
||||
[https://github.com/purpleidea/mgmt/tree/master/examples](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
|
||||
### Systemd:
|
||||
See [`misc/mgmt.service`](misc/mgmt.service) for a sample systemd unit file.
|
||||
This unit file is part of the RPM.
|
||||
|
||||
To specify your custom options for `mgmt` on a systemd distro:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/mgmt.service.d/
|
||||
|
||||
cat > /etc/systemd/system/mgmt.service.d/env.conf <<EOF
|
||||
# Environment variables:
|
||||
MGMT_SEEDS=http://127.0.0.1:2379
|
||||
MGMT_CONVERGED_TIMEOUT=-1
|
||||
MGMT_MAX_RUNTIME=0
|
||||
|
||||
# Other CLI options if necessary.
|
||||
#OPTS="--max-runtime=0"
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
##Development
|
||||
|
||||
This is a project that I started in my free time in 2013. Development is driven
|
||||
by all of our collective patches! Dive right in, and start hacking!
|
||||
Please contact me if you'd like to invite me to speak about this at your event.
|
||||
|
||||
You can follow along [on my technical blog](https://ttboj.wordpress.com/).
|
||||
|
||||
To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt/issues](https://github.com/purpleidea/mgmt/issues).
|
||||
|
||||
##Authors
|
||||
|
||||
Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
|
||||
Please see the
|
||||
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
||||
for more information.
|
||||
|
||||
* [github](https://github.com/purpleidea/)
|
||||
* [@purpleidea](https://twitter.com/#!/purpleidea)
|
||||
* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/)
|
||||
307
Makefile
307
Makefile
@@ -1,28 +1,36 @@
|
||||
# Mgmt
|
||||
# Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
# Copyright (C) 2013-2023+ 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 Affero General Public License as published by
|
||||
# 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 Affero General Public License for more details.
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL = /usr/bin/env bash
|
||||
.PHONY: all art cleanart version program path deps run race generate build 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 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
|
||||
|
||||
# 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"-")
|
||||
OLDGOLANG := $(shell go version | grep -E 'go1.3|go1.4')
|
||||
PKGNAME := $(shell go list .)
|
||||
ifeq ($(VERSION),$(SVERSION))
|
||||
RELEASE = 1
|
||||
else
|
||||
@@ -37,11 +45,44 @@ RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
||||
SERVER = 'dl.fedoraproject.org'
|
||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
|
||||
ifneq ($(GOTAGS),)
|
||||
BUILD_FLAGS = -tags '$(GOTAGS)'
|
||||
endif
|
||||
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
|
||||
@@ -77,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
|
||||
@@ -99,47 +140,82 @@ race:
|
||||
generate:
|
||||
go generate
|
||||
|
||||
build: $(PROGRAM)
|
||||
lang: ## generates the lexer/parser for the language frontend
|
||||
@# recursively run make in child dir named lang
|
||||
@$(MAKE) --quiet -C lang
|
||||
|
||||
$(PROGRAM): main.go
|
||||
@echo "Building: $(PROGRAM), version: $(SVERSION)..."
|
||||
ifneq ($(OLDGOLANG),)
|
||||
@# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
time go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o $(PROGRAM);
|
||||
else
|
||||
time go build -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o $(PROGRAM);
|
||||
endif
|
||||
# build a `mgmt` binary for current host os/arch
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
||||
cp -a $< $@
|
||||
|
||||
$(PROGRAM).static: main.go
|
||||
$(PROGRAM).static: $(GO_FILES) $(MCL_FILES) go.mod go.sum
|
||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||
go generate
|
||||
ifneq ($(OLDGOLANG),)
|
||||
@# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program $(PROGRAM) -X main.version $(SVERSION)' -o $(PROGRAM).static;
|
||||
else
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION)' -o $(PROGRAM).static;
|
||||
endif
|
||||
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);
|
||||
|
||||
clean:
|
||||
build: LDFLAGS=-s -w ## build a fresh mgmt binary
|
||||
build: $(PROGRAM)
|
||||
|
||||
build-debug: LDFLAGS=
|
||||
build-debug: $(PROGRAM)
|
||||
|
||||
# pattern rule target for (cross)building, mgmt-OS-ARCH will be expanded to the correct build
|
||||
# extract os and arch from target pattern
|
||||
GOOS=$(firstword $(subst -, ,$*))
|
||||
GOARCH=$(lastword $(subst -, ,$*))
|
||||
build/mgmt-%: $(GO_FILES) $(MCL_FILES) go.mod go.sum | lang funcgen
|
||||
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
||||
@time env GOOS=${GOOS} GOARCH=${GOARCH} go build -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
|
||||
# to use this you might want to run something like:
|
||||
# GOOSARCHES='linux/arm64' GOTAGS='noaugeas novirt' make crossbuild
|
||||
# and the output will end up in build/
|
||||
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||
crossbuild: ${crossbuild_targets}
|
||||
|
||||
clean: ## clean things up
|
||||
$(MAKE) --quiet -C test 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:
|
||||
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)
|
||||
test_suites=$(shell find test/ -maxdepth 1 -name test-* -exec basename {} .sh \;)
|
||||
# allow to run only one test suite at a time
|
||||
${test_suites}: test-%: build
|
||||
./test.sh $*
|
||||
|
||||
# targets to run individual shell tests (eg: make test-shell-load0)
|
||||
test_shell=$(shell find test/shell/ -maxdepth 1 -name "*.sh" -exec basename {} .sh \;)
|
||||
$(addprefix test-shell-,${test_shell}): test-shell-%: build
|
||||
./test/test-shell.sh "$*.sh"
|
||||
|
||||
gofmt:
|
||||
find . -maxdepth 3 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
|
||||
# TODO: remove gofmt once goimports has a -s option
|
||||
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: DOCUMENTATION.md
|
||||
pandoc DOCUMENTATION.md -o '$(PROGRAM)-documentation.pdf'
|
||||
$(PROGRAM)-documentation.pdf: docs/documentation.md
|
||||
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
|
||||
|
||||
#
|
||||
# build aliases
|
||||
@@ -161,7 +237,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
|
||||
|
||||
#
|
||||
@@ -183,7 +259,7 @@ $(SRPM): $(SPEC) $(SOURCE)
|
||||
#
|
||||
$(SPEC): rpmbuild/ spec.in
|
||||
@echo Running templater...
|
||||
#cat spec.in > $(SPEC)
|
||||
cat spec.in > $(SPEC)
|
||||
sed -e s/__PROGRAM__/$(PROGRAM)/g -e s/__VERSION__/$(VERSION)/g -e s/__RELEASE__/$(RELEASE)/g < spec.in > $(SPEC)
|
||||
# append a changelog to the .spec file
|
||||
git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC)
|
||||
@@ -266,10 +342,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)
|
||||
|
||||
#
|
||||
# tag
|
||||
#
|
||||
tag: ## tags a new release
|
||||
./misc/tag.sh
|
||||
|
||||
#
|
||||
# mkosi
|
||||
#
|
||||
mkosi: mkosi_fedora-30 mkosi_fedora-29 mkosi_centos-7 mkosi_debian-10 mkosi_ubuntu-bionic mkosi_archlinux ## builds distro packages via mkosi
|
||||
|
||||
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: ## 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
|
||||
|
||||
200
README.md
200
README.md
@@ -2,112 +2,134 @@
|
||||
|
||||
[](art/)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](DOCUMENTATION.md)
|
||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
[](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||
[](https://aur.archlinux.org/packages/mgmt/)
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](https://github.com/purpleidea/mgmt/actions/)
|
||||
[](https://godocs.io/github.com/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:
|
||||
Come join us on IRC in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode!
|
||||
You may like the [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) hashtag if you're on [Twitter](https://twitter.com/#!/purpleidea).
|
||||
|
||||
Come join us in the `mgmt` community!
|
||||
|
||||
| Medium | Link |
|
||||
|---|---|
|
||||
| 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:
|
||||
Mgmt is a fairly new project.
|
||||
We're working towards being minimally useful for production environments.
|
||||
We aren't feature complete for what we'd consider a 1.x release yet.
|
||||
With your help you'll be able to influence our design and get us there sooner!
|
||||
|
||||
## Questions:
|
||||
Please join the [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) IRC community!
|
||||
If you have a well phrased question that might benefit others, consider asking it by sending a patch to the documentation [FAQ](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md#usage-and-frequently-asked-questions) section. I'll merge your question, and a patch with the answer!
|
||||
Mgmt is a next generation automation tool. It has similarities to other tools in
|
||||
the configuration management space, but has a fast, modern, distributed systems
|
||||
approach. The project contains an engine and a language.
|
||||
[Please have a look at an introductory video or blog post.](docs/on-the-web.md)
|
||||
|
||||
## Quick start:
|
||||
* Make sure you have golang version 1.6 or greater installed.
|
||||
* If you do not have a GOPATH yet, create one and export it:
|
||||
```
|
||||
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).
|
||||
* Next download the mgmt code base, and switch to that directory:
|
||||
```
|
||||
go get -u github.com/purpleidea/mgmt
|
||||
cd $GOPATH/src/github.com/purpleidea/mgmt
|
||||
```
|
||||
* Get the remaining golang deps with `go get ./...`, or run `make deps` if you're comfortable with how we install them.
|
||||
* Run `make build` to get a freshly built `mgmt` binary.
|
||||
* Run `time ./mgmt run --yaml examples/graph0.yaml --converged-timeout=5 --tmp-prefix` to try out a very simple example!
|
||||
* To run continuously in the default mode of operation, omit the `--converged-timeout` option.
|
||||
* Have fun hacking on our future technology!
|
||||
|
||||
## Examples:
|
||||
Please look in the [examples/](examples/) folder for more examples!
|
||||
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 users should read the [quick start guide](docs/quick-start-guide.md).
|
||||
|
||||
## Documentation:
|
||||
Please see: the manually created [DOCUMENTATION.md](DOCUMENTATION.md) (also available as [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md)) and the automatically generated [GoDoc documentation](https://godoc.org/github.com/purpleidea/mgmt).
|
||||
|
||||
## Roadmap:
|
||||
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!
|
||||
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 read, enjoy and help improve our documentation!
|
||||
|
||||
| Documentation | Additional Notes |
|
||||
|---|---|
|
||||
| [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 |
|
||||
| [function guide](docs/function-guide.md) | for mgmt developers |
|
||||
| [resource guide](docs/resource-guide.md) | for mgmt developers |
|
||||
| [style guide](docs/style-guide.md) | for mgmt developers |
|
||||
| [godoc API reference](https://godoc.org/github.com/purpleidea/mgmt) | for mgmt developers |
|
||||
| [prometheus guide](docs/prometheus.md) | for everyone |
|
||||
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
|
||||
| [development](docs/development.md) | for mgmt developers |
|
||||
| [videos](docs/on-the-web.md) | for everyone |
|
||||
| [blogs](docs/on-the-web.md) | for everyone |
|
||||
|
||||
## Questions:
|
||||
|
||||
Please ask in the [community](#community)!
|
||||
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!
|
||||
|
||||
## 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 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://ttboj.wordpress.com/2016/02/15/debugging-golang-programs/).
|
||||
|
||||
## Dependencies:
|
||||
* golang 1.6 or higher (required, available in most distros)
|
||||
* golang libraries (required, available with `go get`)
|
||||
```
|
||||
go get github.com/coreos/etcd/client
|
||||
go get gopkg.in/yaml.v2
|
||||
go get gopkg.in/fsnotify.v1
|
||||
go get github.com/urfave/cli
|
||||
go get github.com/coreos/go-systemd/dbus
|
||||
go get github.com/coreos/go-systemd/util
|
||||
go get github.com/coreos/pkg/capnslog
|
||||
go get github.com/rgbkrk/libvirt-go
|
||||
```
|
||||
* stringer (optional for building), available as a package on some platforms, otherwise via `go get`
|
||||
```
|
||||
go get golang.org/x/tools/cmd/stringer
|
||||
```
|
||||
* pandoc (optional, for building a pdf of the documentation)
|
||||
* graphviz (optional, for building a visual representation of the graph)
|
||||
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).
|
||||
Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/).
|
||||
|
||||
## Patches:
|
||||
|
||||
We'd love to have your patches! Please send them by email, or as a pull request.
|
||||
|
||||
## On the web:
|
||||
* James Shubin; blog: [Next generation configuration mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* James Shubin; video: [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1)
|
||||
* James Shubin; video: [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1)
|
||||
* Julian Dunn; video: [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1)
|
||||
* Walter Heck; slides: [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3)
|
||||
* Marco Marongiu; blog: [On mgmt](http://syslog.me/2016/02/15/leap-or-die/)
|
||||
* Felix Frank; blog: [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/)
|
||||
* James Shubin; blog: [Automatic edges in mgmt (...and the pkg resource)](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
* James Shubin; blog: [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
* John Arundel; tweet: [“Puppet’s days are numbered.”](https://twitter.com/bitfield/status/732157519142002688)
|
||||
* Felix Frank; blog: [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/)
|
||||
* Felix Frank; blog: [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/)
|
||||
* James Shubin; blog: [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
* James Shubin; video: [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1)
|
||||
* James Shubin; video: [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf))
|
||||
* Felix Frank; blog: [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/)
|
||||
* Felix Frank; blog: [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/)
|
||||
* James Shubin; video: [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1)
|
||||
* James Shubin; blog: [Remote execution in mgmt](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/)
|
||||
* James Shubin; video: [Recording from High Load Strategy 2016](https://vimeo.com/191493409)
|
||||
* James Shubin; video: [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1)
|
||||
* James Shubin; blog: [Send/Recv in mgmt](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/)
|
||||
|
||||
##
|
||||
[Blog posts and recorded talks about mgmt are listed here!](docs/on-the-web.md)
|
||||
|
||||
Happy hacking!
|
||||
|
||||
85
TODO.md
85
TODO.md
@@ -1,67 +1,90 @@
|
||||
# TODO
|
||||
If you're looking for something to do, look here!
|
||||
Let us know if you're working on one of the items.
|
||||
|
||||
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
|
||||
|
||||
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
|
||||
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
|
||||
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
|
||||
- [ ] install signal blocker [bug](https://github.com/hughsie/PackageKit/issues/109)
|
||||
|
||||
## File resource [bug](https://github.com/purpleidea/mgmt/issues/13) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] chown/chmod support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] user/group support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
## File resource [bug](https://github.com/purpleidea/mgmt/issues/64) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
- [ ] recurse limit support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
|
||||
|
||||
## Svc resource
|
||||
- [ ] base resource improvements
|
||||
|
||||
- [ ] refreshonly support [:heart:](https://github.com/purpleidea/mgmt/issues/464)
|
||||
|
||||
## Exec resource
|
||||
|
||||
- [ ] base resource improvements
|
||||
|
||||
## Timer resource
|
||||
- [ ] reset on recompile
|
||||
|
||||
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## User/Group resource
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Virt (libvirt) resource
|
||||
- [ ] base resource [bug](https://github.com/purpleidea/mgmt/issues/25)
|
||||
|
||||
## 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
|
||||
|
||||
- [ ] 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
|
||||
|
||||
- [ ] base plumbing
|
||||
|
||||
## GPG/Auth improvements
|
||||
|
||||
- [ ] base plumbing
|
||||
|
||||
## Resource improvements
|
||||
|
||||
- [ ] more reversible resources implemented
|
||||
- [ ] more "cloud" resources
|
||||
|
||||
## Language improvements
|
||||
- [ ] language design
|
||||
- [ ] lexer/parser
|
||||
|
||||
- [ ] more core functions
|
||||
- [ ] automatic language formatter, ala `gofmt`
|
||||
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
||||
- [ ] vim syntax highlighting
|
||||
- [ ] emacs syntax highlighting
|
||||
- [ ] 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!
|
||||
|
||||
51
Vagrantfile
vendored
Normal file
51
Vagrantfile
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
Vagrant.configure(2) do |config|
|
||||
config.ssh.forward_agent = true
|
||||
config.ssh.username = 'vagrant'
|
||||
config.vm.network "private_network", ip: "192.168.219.2"
|
||||
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
config.vm.define "mgmt-dev" do |instance|
|
||||
instance.vm.box = "bento/fedora-31"
|
||||
end
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.memory = 1536
|
||||
v.cpus = 2
|
||||
end
|
||||
config.vm.provider "libvirt" do |v|
|
||||
v.memory = 2048
|
||||
end
|
||||
|
||||
config.vm.provision "file", source: "vagrant/motd", destination: ".motd"
|
||||
config.vm.provision "shell", inline: "cp ~vagrant/.motd /etc/motd"
|
||||
|
||||
config.vm.provision "file", source: "vagrant/mgmt.bashrc", destination: ".mgmt.bashrc"
|
||||
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
|
||||
|
||||
config.vm.provision "shell", inline: "dnf install -y golang git make"
|
||||
|
||||
# set up packagekit
|
||||
config.vm.provision "shell" do |shell|
|
||||
shell.inline = <<-SCRIPT
|
||||
dnf install -y PackageKit
|
||||
systemctl enable packagekit
|
||||
systemctl start packagekit
|
||||
SCRIPT
|
||||
end
|
||||
|
||||
# set up vagrant home
|
||||
script = <<-SCRIPT
|
||||
grep -q 'mgmt\.bashrc' ~/.bashrc || echo '. ~/.mgmt.bashrc' >>~/.bashrc
|
||||
. ~/.mgmt.bashrc
|
||||
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|
|
||||
shell.privileged = false
|
||||
shell.inline = script
|
||||
end
|
||||
end
|
||||
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,18 +1,18 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2023+ 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 Affero General Public License as published by
|
||||
// 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 Affero General Public License for more details.
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// 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 converger is a facility for reporting the converged state.
|
||||
@@ -20,134 +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() ConvergerUID
|
||||
IsConverged(ConvergerUID) bool // is the UID converged ?
|
||||
SetConverged(ConvergerUID, bool) error // set the converged state of the UID
|
||||
Unregister(ConvergerUID)
|
||||
Start()
|
||||
Pause()
|
||||
Loop(bool)
|
||||
ConvergedTimer(ConvergerUID) <-chan time.Time
|
||||
Status() map[uint64]bool
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
SetStateFn(func(bool) error) // sets the stateFn
|
||||
}
|
||||
|
||||
// ConvergerUID 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 ConvergerUID 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
|
||||
}
|
||||
|
||||
// convergerUID is an implementation of the ConvergerUID interface
|
||||
type convergerUID struct {
|
||||
converger Converger
|
||||
id uint64
|
||||
name string // user defined, friendly name
|
||||
mutex sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the above timer running?
|
||||
}
|
||||
|
||||
// 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 ConvergerUID to the caller
|
||||
func (obj *converger) Register() ConvergerUID {
|
||||
// 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 &convergerUID{
|
||||
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 ConvergerUID) bool {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("Id of ConvergerUID(%s) is nil!", uid.Name()))
|
||||
}
|
||||
obj.mutex.RLock()
|
||||
isConverged, found := obj.status[uid.ID()] // lookup
|
||||
obj.mutex.RUnlock()
|
||||
if !found {
|
||||
panic("Id of ConvergerUID is unregistered!")
|
||||
}
|
||||
return isConverged
|
||||
}
|
||||
|
||||
// SetConverged updates the converger with the converged state of the UID
|
||||
func (obj *converger) SetConverged(uid ConvergerUID, isConverged bool) error {
|
||||
if !uid.IsValid() {
|
||||
return fmt.Errorf("Id of ConvergerUID(%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("Id of ConvergerUID 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
|
||||
}
|
||||
@@ -155,192 +277,170 @@ func (obj *converger) isConverged() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Unregister dissociates the ConvergedUID from the converged checking
|
||||
func (obj *converger) Unregister(uid ConvergerUID) {
|
||||
if !uid.IsValid() {
|
||||
panic(fmt.Sprintf("Id of ConvergerUID(%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 appears 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 ConvergerUID) <-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 *convergerUID) 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 convergerUID.
|
||||
func (obj *convergerUID) 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 convergerUID.
|
||||
func (obj *convergerUID) 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 *convergerUID) IsValid() bool {
|
||||
return obj.id != 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// InvalidateID marks the id as no longer valid
|
||||
func (obj *convergerUID) InvalidateID() {
|
||||
obj.id = 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// IsConverged is a helper function to the regular IsConverged method
|
||||
func (obj *convergerUID) IsConverged() bool {
|
||||
return obj.converger.IsConverged(obj)
|
||||
}
|
||||
|
||||
// SetConverged is a helper function to the regular SetConverged notification
|
||||
func (obj *convergerUID) SetConverged(isConverged bool) error {
|
||||
return obj.converger.SetConverged(obj, isConverged)
|
||||
}
|
||||
|
||||
// Unregister is a helper function to unregister myself
|
||||
func (obj *convergerUID) Unregister() {
|
||||
obj.converger.Unregister(obj)
|
||||
}
|
||||
|
||||
// ConvergedTimer is a helper around the regular ConvergedTimer method
|
||||
func (obj *convergerUID) ConvergedTimer() <-chan time.Time {
|
||||
return obj.converger.ConvergedTimer(obj)
|
||||
}
|
||||
|
||||
// StartTimer runs an invisible timer that automatically converges on timeout.
|
||||
func (obj *convergerUID) StartTimer() (func() error, error) {
|
||||
obj.mutex.Lock()
|
||||
if !obj.running {
|
||||
obj.timer = make(chan struct{})
|
||||
obj.running = true
|
||||
} else {
|
||||
obj.mutex.Unlock()
|
||||
return obj.StopTimer, fmt.Errorf("Timer already started!")
|
||||
// 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)
|
||||
}
|
||||
obj.mutex.Unlock()
|
||||
return util.TimeAfterOrBlock(int(obj.timeout))
|
||||
}
|
||||
|
||||
// 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()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
return obj.StopTimer, fmt.Errorf("timer already started")
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -348,8 +448,8 @@ func (obj *convergerUID) 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -358,25 +458,26 @@ func (obj *convergerUID) StartTimer() (func() error, error) {
|
||||
return obj.StopTimer, nil
|
||||
}
|
||||
|
||||
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
||||
func (obj *convergerUID) ResetTimer() error {
|
||||
// ResetTimer resets the timer to zero.
|
||||
func (obj *UID) ResetTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
obj.timer <- struct{}{} // send the reset message
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Timer hasn't been started!")
|
||||
return fmt.Errorf("timer hasn't been started")
|
||||
}
|
||||
|
||||
// StopTimer stops the running timer permanently until a StartTimer is run.
|
||||
func (obj *convergerUID) StopTimer() error {
|
||||
// StopTimer stops the running timer.
|
||||
func (obj *UID) StopTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if !obj.running {
|
||||
return fmt.Errorf("Timer isn't running!")
|
||||
return fmt.Errorf("timer isn't running")
|
||||
}
|
||||
close(obj.timer)
|
||||
obj.wg.Wait()
|
||||
obj.running = false
|
||||
return nil
|
||||
}
|
||||
|
||||
31
converger/converger_test.go
Normal file
31
converger/converger_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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/>.
|
||||
|
||||
//go:build !root
|
||||
|
||||
package converger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
7
debian/.gitignore
vendored
Normal file
7
debian/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
*.debhelper.log
|
||||
*debhelper
|
||||
changelog
|
||||
debhelper-build-stamp
|
||||
files
|
||||
mgmt.substvars
|
||||
mgmt/*
|
||||
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@@ -0,0 +1 @@
|
||||
9
|
||||
17
debian/control
vendored
Normal file
17
debian/control
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
Source: mgmt
|
||||
Maintainer: Johan Bloemberg (aequitas) <mgmt@ijohan.nl>
|
||||
Build-Depends:
|
||||
debhelper,
|
||||
devscripts,
|
||||
dh-golang,
|
||||
dh-systemd,
|
||||
golang-go,
|
||||
|
||||
Package: mgmt
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, packagekit
|
||||
Suggests: graphviz
|
||||
Description: mgmt: next generation config management!
|
||||
The mgmt tool is a next generation config management prototype. It's
|
||||
not yet ready for production, but we hope to get there soon. Get
|
||||
involved today!
|
||||
21
debian/copyright
vendored
Normal file
21
debian/copyright
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: mgmt
|
||||
Source: <https://github.com/purpleidea/mgmt>
|
||||
|
||||
Files: *
|
||||
Copyright: Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||
License: GPL-3.0
|
||||
|
||||
License: GPL-3.0
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
11
debian/mgmt.docs
vendored
Normal file
11
debian/mgmt.docs
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
AUTHORS
|
||||
COPYING
|
||||
COPYRIGHT
|
||||
README.md
|
||||
THANKS
|
||||
TODO.md
|
||||
docs
|
||||
examples
|
||||
misc/bashrc.sh
|
||||
misc/delta-cpu.sh
|
||||
misc/mgmt.service
|
||||
2
debian/mgmt.install
vendored
Normal file
2
debian/mgmt.install
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
mgmt usr/bin
|
||||
misc/mgmt.service /lib/systemd/system
|
||||
15
debian/rules
vendored
Executable file
15
debian/rules
vendored
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
export DH_OPTIONS
|
||||
export DH_GOPKG := mgmt
|
||||
export DH_GOLANG_INSTALL_ALL := 1
|
||||
unexport GOROOT
|
||||
|
||||
override_dh_auto_build:
|
||||
make build
|
||||
|
||||
override_dh_auto_test:
|
||||
@echo "Tests are disabled for now"
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd
|
||||
8
doc.go
8
doc.go
@@ -1,18 +1,18 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Copyright (C) 2013-2023+ 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 Affero General Public License as published by
|
||||
// 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 Affero General Public License for more details.
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// 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 main provides the main entrypoint for using the `mgmt` software.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM golang:1.6.2
|
||||
FROM golang:1.18
|
||||
|
||||
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 2016-05-10
|
||||
ENV REFRESHED_AT 2020-09-23
|
||||
|
||||
# Update the package list to be able to use required packages
|
||||
RUN apt-get update
|
||||
|
||||
12
docker/Dockerfile.build
Normal file
12
docker/Dockerfile.build
Normal file
@@ -0,0 +1,12 @@
|
||||
FROM centos:7
|
||||
MAINTAINER Karim Boumedhel <karimboumedhel@gmail.com>
|
||||
|
||||
ENV GOPATH=/root/gopath
|
||||
ENV PATH=/opt/rh/rh-ruby22/root/usr/bin:/root/gopath/bin:/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/go/bin
|
||||
ENV LD_LIBRARY_PATH=/opt/rh/rh-ruby22/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
|
||||
ENV PKG_CONFIG_PATH=/opt/rh/rh-ruby22/root/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}
|
||||
|
||||
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.18.5.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.18.5.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.18.5.linux-amd64.tar.gz
|
||||
RUN mkdir -p $GOPATH/src/github.com/purpleidea && cd $GOPATH/src/github.com/purpleidea && git clone --recursive https://github.com/purpleidea/mgmt
|
||||
RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && cd $GOPATH/src/github.com/purpleidea/mgmt && make deps && make build
|
||||
CMD ["/bin/bash"]
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM golang:1.6.2
|
||||
FROM golang:1.18
|
||||
|
||||
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 2016-05-14
|
||||
ENV REFRESHED_AT 2019-02-06
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
|
||||
9
docker/Dockerfile.static
Normal file
9
docker/Dockerfile.static
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM centos:7
|
||||
MAINTAINER Karim Boumedhel <karimboumedhel@gmail.com>
|
||||
|
||||
RUN yum -y install augeas-libs libvirt-libs && yum clean all
|
||||
ADD mgmt /usr/bin
|
||||
RUN chmod 700 /usr/bin/mgmt
|
||||
|
||||
ENTRYPOINT ["/usr/bin/mgmt"]
|
||||
CMD ["-h"]
|
||||
18
docker/scripts/exec-development
Executable file
18
docker/scripts/exec-development
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# runs command provided as argument inside a development (Linux) Docker container
|
||||
|
||||
# Stop on any error
|
||||
set -e
|
||||
|
||||
script_directory="$( cd "$( dirname "$0" )" && pwd )"
|
||||
project_directory=$script_directory/../..
|
||||
|
||||
# Specify the Docker image name
|
||||
image_name='purpleidea/mgmt:development'
|
||||
|
||||
# Run container in development mode
|
||||
docker run --rm --name=mgm_development --user=mgmt \
|
||||
-v "$project_directory:/go/src/github.com/purpleidea/mgmt/" \
|
||||
-w /go/src/github.com/purpleidea/mgmt/ \
|
||||
-it "$image_name" /bin/bash -c "$*"
|
||||
2
docs/.gitignore
vendored
Normal file
2
docs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
mgmt-documentation.pdf
|
||||
_build
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = mgmt
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
158
docs/conf.py
Normal file
158
docs/conf.py
Normal file
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# mgmt documentation build configuration file, created by
|
||||
# sphinx-quickstart on Wed Feb 15 21:34:09 2017.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
from recommonmark.parser import CommonMarkParser
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
#
|
||||
|
||||
source_parsers = {
|
||||
'.md': CommonMarkParser,
|
||||
}
|
||||
|
||||
source_suffix = ['.rst', '.md']
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'mgmt'
|
||||
copyright = u'2013-2023+ James Shubin and the project contributors'
|
||||
author = u'James Shubin'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = u''
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = u''
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'venv']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
#html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
|
||||
# -- Options for HTMLHelp output ------------------------------------------
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'mgmtdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#
|
||||
# 'preamble': '',
|
||||
|
||||
# Latex figure (float) alignment
|
||||
#
|
||||
# 'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'mgmt.tex', u'mgmt Documentation',
|
||||
u'James Shubin', 'manual'),
|
||||
]
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'mgmt', u'mgmt Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'mgmt', u'mgmt Documentation',
|
||||
author, 'mgmt', 'A next generation config management prototype!',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
162
docs/development.md
Normal file
162
docs/development.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# Development
|
||||
|
||||
This document contains some additional information and help regarding
|
||||
developing `mgmt`. Useful tools, conventions, etc.
|
||||
|
||||
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.18 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
|
||||
|
||||
This project has both unit tests in the form of golang tests and integration
|
||||
tests using shell scripting.
|
||||
|
||||
Native golang tests are preferred over tests written in our shell testing
|
||||
framework. Please see [https://golang.org/pkg/testing/](https://golang.org/pkg/testing/)
|
||||
for more information.
|
||||
|
||||
To run all tests:
|
||||
|
||||
```
|
||||
make test
|
||||
```
|
||||
|
||||
There is a library of quick and small integration tests for the language and
|
||||
YAML related things, check out [`test/shell/`](/test/shell). Adding a test is as
|
||||
easy as copying one of the files in [`test/shell/`](/test/shell) and adapting
|
||||
it.
|
||||
|
||||
This test suite won't run by default (unless when on CI server) and needs to be
|
||||
called explictly using:
|
||||
|
||||
```
|
||||
make test-shell
|
||||
```
|
||||
|
||||
Or run an individual shell test using:
|
||||
|
||||
```
|
||||
make test-shell-load0
|
||||
```
|
||||
|
||||
Tip: you can use TAB completion with `make` to quickly get a list of possible
|
||||
individual tests to run.
|
||||
|
||||
## Tools, integrations, IDE's etc
|
||||
|
||||
### IDE/Editor support
|
||||
|
||||
* Emacs: see `misc/emacs/`
|
||||
* [Textmate](https://github.com/aequitas/mgmt.tmbundle)
|
||||
* [VSCode](https://github.com/aequitas/mgmt.vscode)
|
||||
497
docs/documentation.md
Normal file
497
docs/documentation.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# General documentation
|
||||
|
||||
## Overview
|
||||
|
||||
The `mgmt` tool is a next generation config management prototype. It's not yet
|
||||
ready for production, but we hope to get there soon. Get involved today!
|
||||
|
||||
## Project Description
|
||||
|
||||
The mgmt tool is a distributed, event driven, config management tool, that
|
||||
supports parallel execution, and librarification to be used as the management
|
||||
foundation in and for, new and existing software.
|
||||
|
||||
For more information, you may like to read some blog posts from the author:
|
||||
|
||||
* [Next generation config mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* [Automatic edges in mgmt](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/)
|
||||
* [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
* [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
* [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/)
|
||||
* [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/)
|
||||
* [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/)
|
||||
|
||||
There is also an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1)
|
||||
available. Older videos and other material [is available](on-the-web.md).
|
||||
|
||||
## Setup
|
||||
|
||||
You'll probably want to read the [quick start guide](quick-start-guide.md) to
|
||||
get going.
|
||||
|
||||
## Features
|
||||
|
||||
This section details the numerous features of mgmt and some caveats you might
|
||||
need to be aware of.
|
||||
|
||||
### Autoedges
|
||||
|
||||
Automatic edges, or AutoEdges, is the mechanism in mgmt by which it will
|
||||
automatically create dependencies for you between resources. For example,
|
||||
since mgmt can discover which files are installed by a package it will
|
||||
automatically ensure that any file resource you declare that matches a
|
||||
file installed by your package resource will only be processed after the
|
||||
package is installed.
|
||||
|
||||
#### Controlling autoedges
|
||||
|
||||
Though autoedges is likely to be very helpful and avoid you having to declare
|
||||
all dependencies explicitly, there are cases where this behaviour is
|
||||
undesirable.
|
||||
|
||||
Some distributions allow package installations to automatically start the
|
||||
service they ship. This can be problematic in the case of packages like MySQL
|
||||
as there are configuration options that need to be set before MySQL is ever
|
||||
started for the first time (or you'll need to wipe the data directory). In
|
||||
order to handle this situation you can disable autoedges per resource and
|
||||
explicitly declare that you want `my.cnf` to be written to disk before the
|
||||
installation of the `mysql-server` package.
|
||||
|
||||
You can disable autoedges for a resource by setting the `autoedge` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
#### Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/)
|
||||
|
||||
### Autogrouping
|
||||
|
||||
Automatic grouping or AutoGroup is the mechanism in mgmt by which it will
|
||||
automatically group multiple resource vertices into a single one. This is
|
||||
particularly useful for grouping multiple package resources into a single
|
||||
resource, since the multiple installations can happen together in a single
|
||||
transaction, which saves a lot of time because package resources typically have
|
||||
a large fixed cost to running (downloading and verifying the package repo) and
|
||||
if they are grouped they share this fixed cost. This grouping feature can be
|
||||
used for other use cases too.
|
||||
|
||||
You can disable autogrouping for a resource by setting the `autogroup` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
#### Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
|
||||
### Automatic clustering
|
||||
|
||||
Automatic clustering is a feature by which mgmt automatically builds, scales,
|
||||
and manages the embedded etcd cluster which is compiled into mgmt itself. It is
|
||||
quite helpful for rapidly bootstrapping clusters and avoiding the extra work to
|
||||
setup etcd.
|
||||
|
||||
If you prefer to avoid this feature. you can always opt to use an existing etcd
|
||||
cluster that is managed separately from mgmt by pointing your mgmt agents at it
|
||||
with the `--seeds` variable.
|
||||
|
||||
#### Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
|
||||
### Remote ("agent-less") mode
|
||||
|
||||
Remote mode is a special mode that lets you kick off mgmt runs on one or more
|
||||
remote machines which are only accessible via SSH. In this mode the initiating
|
||||
host connects over SSH, copies over the `mgmt` binary, opens an SSH tunnel, and
|
||||
runs the remote program while simultaneously passing the etcd traffic back
|
||||
through the tunnel so that the initiators etcd cluster can be used to exchange
|
||||
resource data.
|
||||
|
||||
The interesting benefit of this architecture is that multiple hosts which can't
|
||||
connect directly use the initiator to pass the important traffic through to each
|
||||
other. Once the cluster has converged all the remote programs can shutdown
|
||||
leaving no residual agent.
|
||||
|
||||
This mode can also be useful for bootstrapping a new host where you'd like to
|
||||
have the service run continuously and as part of an mgmt cluster normally.
|
||||
|
||||
In particular, when combined with the `--converged-timeout` parameter, the
|
||||
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:
|
||||
[https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/)
|
||||
|
||||
### Puppet support
|
||||
|
||||
You can supply a Puppet manifest instead of creating the (YAML) graph manually.
|
||||
Puppet must be installed and in `mgmt`'s search path. You also need the
|
||||
[ffrank-mgmtgraph Puppet module](https://forge.puppet.com/ffrank/mgmtgraph).
|
||||
|
||||
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 --puppet agent`
|
||||
|
||||
2. Compile a local manifest file (like `puppet apply`)
|
||||
|
||||
`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 --puppet 'file { "/etc/ntp.conf": ensure => file }'`
|
||||
|
||||
For more details and caveats see [puppet-guide.md](puppet-guide.md).
|
||||
|
||||
#### Blog post
|
||||
|
||||
An introductory post on the Puppet support is on
|
||||
[Felix's blog](http://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/).
|
||||
|
||||
## Reference
|
||||
|
||||
Please note that there are a number of undocumented options. For more
|
||||
information on these options, please view the source at:
|
||||
[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/).
|
||||
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.
|
||||
|
||||
### Meta parameters
|
||||
|
||||
These meta parameters are special parameters (or properties) which can apply to
|
||||
any resource. The usefulness of doing so will depend on the particular meta
|
||||
parameter and resource combination.
|
||||
|
||||
#### AutoEdge
|
||||
|
||||
Boolean. Should we generate auto edges for this resource?
|
||||
|
||||
#### AutoGroup
|
||||
|
||||
Boolean. Should we attempt to automatically group this resource with others?
|
||||
|
||||
#### Noop
|
||||
|
||||
Boolean. Should the Apply portion of the CheckApply method of the resource
|
||||
make any changes? Noop is a concatenation of no-operation.
|
||||
|
||||
#### Retry
|
||||
|
||||
Integer. The number of times to retry running the resource on error. Use -1 for
|
||||
infinite. This currently applies for both the Watch operation (which can fail)
|
||||
and for the CheckApply operation. While they could have separate values, 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.
|
||||
|
||||
#### Delay
|
||||
|
||||
Integer. Number of milliseconds to wait between retries. The same value is
|
||||
shared between the Watch and CheckApply retries. This currently applies for both
|
||||
the Watch operation (which can fail) and for the CheckApply operation. While
|
||||
they could have separate values, 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.
|
||||
|
||||
#### Poll
|
||||
|
||||
Integer. Number of seconds to wait between `CheckApply` checks. If this is
|
||||
greater than zero, then the standard event based `Watch` mechanism for this
|
||||
resource is replaced with a simple polling mechanism. In general, this is not
|
||||
recommended, unless you have a very good reason for doing so.
|
||||
|
||||
Please keep in mind that if you have a resource which changes every `I` seconds,
|
||||
and you poll it every `J` seconds, and you've asked for a converged timeout of
|
||||
`K` seconds, and `I <= J <= K`, then your graph will likely never converge.
|
||||
|
||||
When polling, the system detects that a resource is not converged if its
|
||||
`CheckApply` method returns false. This allows a resource which changes every
|
||||
`I` seconds, and which is polled every `J` seconds, and with a converged timeout
|
||||
of `K` seconds to still converge when `J <= K`, as long as `I > J || I > K`,
|
||||
which is another way of saying that if the resource finally settles down to give
|
||||
the graph enough time, it can probably converge.
|
||||
|
||||
#### Limit
|
||||
|
||||
Float. Maximum rate of `CheckApply` runs started per second. Useful to limit
|
||||
an especially _eventful_ process from causing excessive checks to run. This
|
||||
defaults to `+Infinity` which adds no limiting. If you change this value, you
|
||||
will also need to change the `Burst` value to a non-zero value. Please see the
|
||||
[rate](https://godoc.org/golang.org/x/time/rate) package for more information.
|
||||
|
||||
#### Burst
|
||||
|
||||
Integer. Burst is the maximum number of runs which can happen without invoking
|
||||
the rate limiter as designated by the `Limit` value. If the `Limit` is not set
|
||||
to `+Infinity`, this must be a non-zero value. Please see the
|
||||
[rate](https://godoc.org/golang.org/x/time/rate) package for more information.
|
||||
|
||||
#### Sema
|
||||
|
||||
List of string ids. Sema is a P/V style counting semaphore which can be used to
|
||||
limit parallelism during the CheckApply phase of resource execution. Each
|
||||
resource can have `N` different semaphores which share a graph global namespace.
|
||||
Each semaphore has a maximum count associated with it. The default value of the
|
||||
size is 1 (one) if size is unspecified. Each string id is the unique id of the
|
||||
semaphore. If the id contains a trailing colon (:) followed by a positive
|
||||
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/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`.
|
||||
|
||||
#### `--converged-timeout <seconds>`
|
||||
|
||||
Exit if the machine has converged for approximately this many seconds.
|
||||
|
||||
#### `--max-runtime <seconds>`
|
||||
|
||||
Exit when the agent has run for approximately this many seconds. This is not
|
||||
generally recommended, but may be useful for users who know what they're doing.
|
||||
|
||||
#### `--noop`
|
||||
|
||||
Globally force all resources into no-op mode. This also disables the export to
|
||||
etcd functionality, but does not disable resource collection, however all
|
||||
resources that are collected will have their individual noop settings set.
|
||||
|
||||
#### `--sema <size>`
|
||||
|
||||
Globally add a counting semaphore of this size to each resource in the graph.
|
||||
The semaphore will get given an id of `:size`. In other words if you specify a
|
||||
size of 42, you can expect a semaphore if named: `:42`. It is expected that
|
||||
consumers of the semaphore metaparameter always include a prefix to avoid a
|
||||
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`.
|
||||
|
||||
#### `--allow-interactive`
|
||||
|
||||
Allow interactive prompting for SSH passwords if there is no authentication
|
||||
method that works.
|
||||
|
||||
#### `--ssh-priv-id-rsa`
|
||||
|
||||
Specify the path for finding SSH keys. This defaults to `~/.ssh/id_rsa`. To
|
||||
never use this method of authentication, set this to the empty string.
|
||||
|
||||
#### `--cconns`
|
||||
|
||||
The maximum number of concurrent remote ssh connections to run. This defaults
|
||||
to `0`, which means unlimited.
|
||||
|
||||
#### `--no-caching`
|
||||
|
||||
Don't allow remote caching of the remote execution binary. This will require
|
||||
the binary to be copied over for every remote execution, but it limits the
|
||||
likelihood that there is leftover information from the configuration process.
|
||||
|
||||
#### `--prefix <path>`
|
||||
|
||||
Specify a path to a custom working directory prefix. This directory will get
|
||||
created if it does not exist. This usually defaults to `/var/lib/mgmt/`. This
|
||||
can't be combined with the `--tmp-prefix` option. It can be combined with the
|
||||
`--allow-tmp-prefix` option.
|
||||
|
||||
#### `--tmp-prefix`
|
||||
|
||||
If this option is specified, a temporary prefix will be used instead of the
|
||||
default prefix. This can't be combined with the `--prefix` option.
|
||||
|
||||
#### `--allow-tmp-prefix`
|
||||
|
||||
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 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
|
||||
|
||||
You can control some compilation variables by using environment variables.
|
||||
|
||||
#### Disable libvirt support
|
||||
|
||||
If you wish to compile mgmt without libvirt, you can use the following command:
|
||||
|
||||
```
|
||||
GOTAGS=novirt make build
|
||||
```
|
||||
|
||||
#### Disable augeas support
|
||||
|
||||
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 nodocker" make build
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
directory in the git source repository. It is available from:
|
||||
|
||||
[https://github.com/purpleidea/mgmt/tree/master/examples](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
|
||||
### Systemd:
|
||||
|
||||
See [`misc/mgmt.service`](misc/mgmt.service) for a sample systemd unit file.
|
||||
This unit file is part of the RPM.
|
||||
|
||||
To specify your custom options for `mgmt` on a systemd distro:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/mgmt.service.d/
|
||||
|
||||
cat > /etc/systemd/system/mgmt.service.d/env.conf <<EOF
|
||||
# Environment variables:
|
||||
MGMT_SEEDS=http://127.0.0.1:2379
|
||||
MGMT_CONVERGED_TIMEOUT=-1
|
||||
MGMT_MAX_RUNTIME=0
|
||||
|
||||
# Other CLI options if necessary.
|
||||
#OPTS="--max-runtime=0"
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
This is a project that I started in my free time in 2013. Development is driven
|
||||
by all of our collective patches! Dive right in, and start hacking!
|
||||
Please contact me if you'd like to invite me to speak about this at your event.
|
||||
|
||||
You can follow along [on my technical blog](https://purpleidea.com/blog/).
|
||||
|
||||
To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt/issues](https://github.com/purpleidea/mgmt/issues).
|
||||
|
||||
## Authors
|
||||
|
||||
Copyright (C) 2013-2023+ James Shubin and the project contributors
|
||||
|
||||
Please see the
|
||||
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
||||
for more information.
|
||||
|
||||
* [github](https://github.com/purpleidea/)
|
||||
* [@purpleidea](https://twitter.com/#!/purpleidea)
|
||||
* [https://purpleidea.com/](https://purpleidea.com/)
|
||||
382
docs/faq.md
Normal file
382
docs/faq.md
Normal file
@@ -0,0 +1,382 @@
|
||||
## Frequently asked questions
|
||||
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
|
||||
### Why did you start this project?
|
||||
|
||||
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
|
||||
broadly divided into two main categories:
|
||||
|
||||
1. With contributions written in `golang`
|
||||
2. With contributions _not_ written in `golang`
|
||||
|
||||
If you do not know `golang`, and have no desire to learn, you can still
|
||||
contribute to mgmt by using it, testing it, writing docs, or even just by
|
||||
telling your friends about it. If you don't mind some coding, learning about the
|
||||
[mgmt language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)
|
||||
might be an enjoyable experience for you. It is a small [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
|
||||
and not a general purpose programming language, and you might find it more fun
|
||||
than what you're typically used to. One of the reasons the mgmt author got into
|
||||
writing automation modules, was because he found it much more fun to build with
|
||||
a higher level DSL, than in a general purpose programming language.
|
||||
|
||||
If you do not know `golang`, and would like to learn, are a beginner and want to
|
||||
improve your skills, or want to gain some great interdisciplinary systems
|
||||
engineering knowledge around a cool automation project, we're happy to mentor
|
||||
you. Here are some pre-requisites steps which we recommend:
|
||||
|
||||
1. Make sure you have a somewhat recent GNU/Linux environment to hack on. A
|
||||
recent [Fedora](https://getfedora.org/) or [Debian](https://www.debian.org/)
|
||||
environment is recommended. Developing, testing, and contributing on `macOS` or
|
||||
`Windows` will be either more difficult or impossible.
|
||||
2. Ensure that you're mildly comfortable with the basics of using `git`. You can
|
||||
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://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
|
||||
something interests you. If you find one, let us know you're working on it by
|
||||
leaving a comment in the ticket. We'll be around to answer questions in the IRC
|
||||
channel, and to create new issues if there wasn't something that fit your
|
||||
interests. When you submit a patch, we'll review it and give you some feedback.
|
||||
Over time, we hope you'll learn a lot while supporting the project! Now get
|
||||
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.
|
||||
|
||||
Because we have not released a `1.0` release yet, we are not guaranteeing
|
||||
stability of the internal or external API's. We only change them if it's really
|
||||
necessary, and we don't expect anything particularly drastic to occur. We would
|
||||
expect it to be relatively easy to adapt your code if such changes happened.
|
||||
|
||||
As with all software, bugs can occur, and while we make no guarantees of being
|
||||
bug-free, there are a number of things we've done to reduce the chances of one
|
||||
causing you trouble:
|
||||
|
||||
1. Our software is written in golang, which is a memory-safe language, and which
|
||||
is known to reduce or eliminate entire classes of bugs.
|
||||
2. We have a test suite which we run on every commit, and every 24 hours. If you
|
||||
have a particular case that you'd like to test, you are welcome to add it in!
|
||||
3. The mgmt language itself offers a number of safety features. You can
|
||||
[read about them in the introductory blog post](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/).
|
||||
|
||||
Having said all this, as with all software, there are still missing features
|
||||
which some users might want in their production environments. We're working hard
|
||||
to get all of those implemented, but we hope that you'll get involved and help
|
||||
us finish off the ones that are most important to you. We are happy to mentor
|
||||
new contributors, and have even [tagged](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
|
||||
a number of issues if you need help getting started.
|
||||
|
||||
Some of the current limitations include:
|
||||
|
||||
* Auth hasn't been implemented yet, so you should only use it in trusted
|
||||
environments (not on publicly accessible networks) for now.
|
||||
* The number of built-in core functions is still small. You may encounter
|
||||
scenarios where you're missing a function. The good news is that it's relatively
|
||||
easy to add this missing functionality yourself. In time, with your help, the
|
||||
list will grow!
|
||||
* Large file distribution is not yet implemented. You might want a scenario
|
||||
where mgmt is used to distribute large files (such as `.iso` images) throughout
|
||||
your cluster. While this isn't a common use-case, it won't be possible until
|
||||
someone wants to write the patch. (Mentoring available!) You can workaround this
|
||||
easily by storing those files on a separate fileserver for the interim.
|
||||
* There isn't an ecosystem of community `modules` yet. We've got this on our
|
||||
roadmap, so please stay tuned!
|
||||
|
||||
We hope you'll participate as an early adopter. Every additional pair of helping
|
||||
hands gets us all there faster! It's quite possible to use this to build useful
|
||||
automation today, and we hope you'll start getting familiar with the software.
|
||||
|
||||
### Why did you use etcd? What about consul?
|
||||
|
||||
Etcd and consul are both written in golang, which made them the top two
|
||||
contenders for my prototype. Ultimately a choice had to be made, and etcd was
|
||||
chosen, but it was also somewhat arbitrary. If there is available interest,
|
||||
good reasoning, *and* patches, then we would consider either switching or
|
||||
supporting both, but this is not a high priority at this time.
|
||||
|
||||
### Can I use an existing etcd cluster instead of the automatic embedded servers?
|
||||
|
||||
Yes, it's possible to use an existing etcd cluster instead of the automatic,
|
||||
elastic embedded etcd servers. To do so, simply point to the cluster with the
|
||||
`--seeds` variable, the same way you would if you were seeding a new member to
|
||||
an existing mgmt cluster.
|
||||
|
||||
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
|
||||
converged state, even as external forces change the current state, or as your
|
||||
time-varying desired state changes over time. (You can write code in the mgmt
|
||||
language which will let you describe a desired state which might change over
|
||||
time.)
|
||||
|
||||
Some users might prefer to only run `mgmt` on-demand manually, or at a set
|
||||
interval via a tool like `cron`. In order to do so, `mgmt` must have a way to
|
||||
shut itself down after a single "run". This feature is possible with the
|
||||
`--converged-timeout` flag. You may specify this flag, along with a number of
|
||||
seconds as the argument, and when there has been no activity for that many
|
||||
seconds, the program will shutdown.
|
||||
|
||||
Alternatively, while it is not recommended, if you'd like to ensure the program
|
||||
never runs for longer that a specific number of seconds, you can ask it to
|
||||
shutdown after that time interval using the `--max-runtime` flag. This also
|
||||
requires a number of seconds as an argument.
|
||||
|
||||
#### Example:
|
||||
|
||||
```
|
||||
./mgmt run lang examples/lang/hello0.mcl --converged-timeout=5
|
||||
```
|
||||
|
||||
### 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: server: starting...
|
||||
etcd: server: start timeout of 1m0s reached
|
||||
etcd: server: close timeout of 15s reached
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### On running `make` to build a new version, it errors with: `Text file busy`.
|
||||
|
||||
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?
|
||||
|
||||
Mgmt probably works best on Linux, because that's what most developers use for
|
||||
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`, `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
|
||||
improvements are not automatically brought into the project. Instead, we run our
|
||||
complete test suite against the entire project (with the latest dependencies)
|
||||
[every 24 hours](https://docs.travis-ci.com/user/cron-jobs/) to ensure that it
|
||||
all still works.
|
||||
|
||||
Occasionally a dependency breaks API and causes a failure. In those situations,
|
||||
we're notified almost immediately, it's easy to see exactly which commit caused
|
||||
the breakage, and we can either quickly notify the author (if it was a mistake)
|
||||
or update our code if it was a sensible change. This also puts less burden on
|
||||
authors to support old, legacy versions of their software unnecessarily.
|
||||
|
||||
Historically, we've had approximately one such breakage per year, which were all
|
||||
detected and fixed within a few hours. The cost of these small, rare,
|
||||
interruptions is much less expensive than having to periodically move every
|
||||
dependency in the project to the latest versions. Some examples of this include:
|
||||
|
||||
* We caught the `go-bindata` swap before it was publicly known, and fixed it in:
|
||||
[adbe9c7be178898de3645b0ed17ed2ca06646017](https://github.com/purpleidea/mgmt/commit/adbe9c7be178898de3645b0ed17ed2ca06646017).
|
||||
|
||||
* We caught the `codegangsta/cli` API change improvement, and fixed it in:
|
||||
[ab73261fd4e98cf7ecb08066ad228a8f559ba16a](https://github.com/purpleidea/mgmt/commit/ab73261fd4e98cf7ecb08066ad228a8f559ba16a).
|
||||
|
||||
* We caught an un-announced libvirt API change, and promptly fixed it in:
|
||||
[95cb94a03958a9d2ebf01df0821a8c13a4f3a28c](https://github.com/purpleidea/mgmt/commit/95cb94a03958a9d2ebf01df0821a8c13a4f3a28c).
|
||||
|
||||
If we choose responsible dependencies, then it usually means that those authors
|
||||
are also responsible with their changes to API and to git master. If we ever
|
||||
find that it's not the case, then we will either switch that dependency to a
|
||||
more responsible version, or fork it if necessary.
|
||||
|
||||
Occasionally, we want to pin a dependency to a particular version. This can
|
||||
happen if the project treats `git master` as an unstable branch, or because a
|
||||
dependency needs a newer version of golang than the minimum that we require for
|
||||
our project. In those cases it's sensible to assume the technical debt, and
|
||||
vendor the dependency. The common tools such as `glide` and `godep` work by
|
||||
requiring you install their software, and by either storing a yaml file with the
|
||||
version of that dependency in your repository, and/or copying all of that code
|
||||
into git and explicitly storing it. This project thinks that all of these
|
||||
solutions are wasteful and unnecessary, particularly when an existing elegant
|
||||
solution already exists: `[git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)`.
|
||||
|
||||
The advantages of using `git submodules` are three-fold:
|
||||
1. You already have the required tools installed.
|
||||
2. You only store a pointer to the dependency, not additional files or code.
|
||||
3. The git submodule tools let you easily switch dependency versions, see diff
|
||||
output, and responsibly plan and test your versions bumps with ease.
|
||||
|
||||
Don't blindly use the tools that others tell you to. Learn what they do, think
|
||||
for yourself, and become a power user today! That process led us to using
|
||||
`git submodules`. Hopefully you'll come to the same conclusions that we did.
|
||||
|
||||
**UPDATE:**
|
||||
|
||||
After golang made it virtually impossible to build without `go.mod` stuff, we've
|
||||
switched to it since golang 1.16. I still think the above approach was better,
|
||||
and that the `go mod` tooling should have been a layer on top of git submodules
|
||||
so that we don't grow yet another lock file format, and existing folks who are
|
||||
comfortable with `git` can use those tools directly.
|
||||
|
||||
### Did you know that there is a band named `MGMT`?
|
||||
|
||||
I didn't realize this when naming the project, and it is accidental. After much
|
||||
anguishing, I chose the name because it was short and I thought it was
|
||||
appropriately descriptive. If you need a less ambiguous search term or phrase,
|
||||
you can try using `mgmtconfig` or `mgmt config`.
|
||||
|
||||
It also doesn't stand for
|
||||
[Methyl Guanine Methyl Transferase](https://en.wikipedia.org/wiki/O-6-methylguanine-DNA_methyltransferase)
|
||||
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://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).
|
||||
461
docs/function-guide.md
Normal file
461
docs/function-guide.md
Normal file
@@ -0,0 +1,461 @@
|
||||
# Function guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `mgmt` tool has built-in functions which add useful, reactive functionality
|
||||
to the language. This guide describes the different function API's that are
|
||||
available. It is meant to instruct developers on how to write new functions.
|
||||
Since `mgmt` and the core functions are written in golang, some prior golang
|
||||
knowledge is assumed.
|
||||
|
||||
## Theory
|
||||
|
||||
Functions in `mgmt` are similar to functions in other languages, however they
|
||||
also have a [reactive](https://en.wikipedia.org/wiki/Functional_reactive_programming)
|
||||
component. Our functions can produce events over time, and there are different
|
||||
ways to write functions. For some background on this design, please read the
|
||||
[original article](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)
|
||||
on the subject.
|
||||
|
||||
## Native Functions
|
||||
|
||||
Native functions are functions which are implemented in the mgmt language
|
||||
itself. These are currently not available yet, but are coming soon. Stay tuned!
|
||||
|
||||
## Simple Function API
|
||||
|
||||
Most functions should be implemented using the simple function API. This API
|
||||
allows you to implement simple, static, [pure](https://en.wikipedia.org/wiki/Pure_function)
|
||||
functions that don't require you to write much boilerplate code. They will be
|
||||
automatically re-evaluated as needed when their input values change. These will
|
||||
all be automatically made available as helper functions within mgmt templates,
|
||||
and are also available for use anywhere inside mgmt programs.
|
||||
|
||||
You'll need some basic knowledge of using the [`types`](https://github.com/purpleidea/mgmt/tree/master/lang/types)
|
||||
library which is included with mgmt. This library lets you interact with the
|
||||
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 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:
|
||||
|
||||
### Example
|
||||
|
||||
```golang
|
||||
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.
|
||||
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
|
||||
return &types.StrValue{
|
||||
V: fmt.Sprintf("%d^2 is %d", i, i * i),
|
||||
}, nil // no serious errors occurred
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
This simple function accepts one `int` as input, and returns one `str`.
|
||||
Functions can have zero or more inputs, and must have exactly one output. You
|
||||
must be sure to use the `types` library correctly, since if you try and access
|
||||
an input which should not exist (eg: `input[2]`, when there are only two
|
||||
that are expected), then you will cause a panic. If you have declared that a
|
||||
particular argument is an `int` but you try to read it with `.Bool()` you will
|
||||
also cause a panic. Lastly, make sure that you return a value in the correct
|
||||
type or you will also cause a panic!
|
||||
|
||||
If anything goes wrong, you can return an error, however this will cause the
|
||||
mgmt engine to shutdown. It should be seen as the equivalent to calling a
|
||||
`panic()`, however it is safer because it brings the engine down cleanly.
|
||||
Ideally, your functions should never need to error. You should never cause a
|
||||
real `panic()`, since this could have negative consequences to the system.
|
||||
|
||||
## Simple Polymorphic Function API
|
||||
|
||||
Most functions should be implemented using the simple function API. If they need
|
||||
to have multiple polymorphic forms under the same name, then you can use this
|
||||
API. This is useful for situations when it would be unhelpful to name the
|
||||
functions differently, or when the number of possible signatures for the
|
||||
function would be infinite.
|
||||
|
||||
The canonical example of this is the `len` function which returns the number of
|
||||
elements in either a `list` or a `map`. Since lists and maps are two different
|
||||
types, you can see that polymorphism is more convenient than requiring a
|
||||
`listlen` and `maplen` function. Nevertheless, it is also required because a
|
||||
`list of int` is a different type than a `list of str`, which is a different
|
||||
type than a `list of list of str` and so on. As you can see the number of
|
||||
possible input types for such a `len` function is infinite.
|
||||
|
||||
Another downside to implementing your functions with this API is that they will
|
||||
*not* be made available for use inside templates. This is a limitation of the
|
||||
`golang` template library. In the future if this limitation proves to be
|
||||
significantly annoying, we might consider writing our own template library.
|
||||
|
||||
As with the simple, non-polymorphic API, you can only implement [pure](https://en.wikipedia.org/wiki/Pure_function)
|
||||
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 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. 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
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// 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,
|
||||
},
|
||||
{
|
||||
T: types.NewType("func({variant: variant}) int"),
|
||||
V: Len,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Len returns the number of elements in a list or the number of key pairs in a
|
||||
// map. It can operate on either of these types.
|
||||
func Len(input []types.Value) (types.Value, error) {
|
||||
var length int
|
||||
switch k := input[0].Type().Kind; k {
|
||||
case types.KindList:
|
||||
length = len(input[0].List())
|
||||
case types.KindMap:
|
||||
length = len(input[0].Map())
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported kind: %+v", k)
|
||||
}
|
||||
|
||||
return &types.IntValue{
|
||||
V: int64(length),
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
This simple polymorphic function can accept an infinite number of signatures, of
|
||||
which there are two basic forms. Both forms return an `int` as is seen above.
|
||||
The first form takes a `[]variant` which means a `list` of `variant`'s, which
|
||||
means that it can be a list of any type, since `variant` itself is not a
|
||||
concrete type. The second form accepts a `{variant: variant}`, which means that
|
||||
it accepts any form of `map` as input.
|
||||
|
||||
The implementation for both of these forms is the same: it is handled by the
|
||||
same `Len` function which is clever enough to be able to deal with any of the
|
||||
type signatures possible from those two patterns.
|
||||
|
||||
At compile time, if your `mcl` code type checks correctly, a concrete type will
|
||||
be known for each and every usage of the `len` function, and specific values
|
||||
will be passed in for this code to compute the length of. As usual, make sure to
|
||||
only write safe code that will not panic! A panic is a bug. If you really cannot
|
||||
continue, then you must return an error.
|
||||
|
||||
## Function API
|
||||
|
||||
To implement a reactive function in `mgmt` it must satisfy the
|
||||
[`Func`](https://github.com/purpleidea/mgmt/blob/master/lang/interfaces/func.go)
|
||||
interface. Using the [Simple Function API](#simple-function-api) is preferable
|
||||
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.
|
||||
|
||||
### Info
|
||||
|
||||
```golang
|
||||
Info() *interfaces.Info
|
||||
```
|
||||
|
||||
This returns some information about the function. It is necessary so that the
|
||||
compiler can type check the code correctly, and know what optimizations can be
|
||||
performed. This is usually the first method which is called by the engine.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
func (obj *FooFunc) Info() *interfaces.Info {
|
||||
return &interfaces.Info{
|
||||
Pure: true,
|
||||
Sig: types.NewType("func(a int) str"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Init
|
||||
|
||||
```golang
|
||||
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 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.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Init runs some startup code for this function.
|
||||
func (obj *FooFunc) Init(init *interfaces.Init) error {
|
||||
obj.init = init
|
||||
obj.closeChan = make(chan struct{}) // shutdown signal
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Close
|
||||
|
||||
```golang
|
||||
Close() error
|
||||
```
|
||||
|
||||
This is called to cleanup the function. It usually causes the stream to
|
||||
shutdown. Even if `Stream()` decided to shutdown early, it might still get
|
||||
called. It is usually called by the engine to tell the function to shutdown.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Close runs some shutdown code for this function and turns off the stream.
|
||||
func (obj *FooFunc) Close() error {
|
||||
close(obj.closeChan) // send a signal to tell the stream to close
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Stream
|
||||
|
||||
```golang
|
||||
Stream() error
|
||||
```
|
||||
|
||||
`Stream` is where the real _work_ is done. This method is started by the
|
||||
language function engine. It will run this function while simultaneously sending
|
||||
it values on the `input` channel. It will only send a complete set of input
|
||||
values. You should send a value to the output channel when you have decided that
|
||||
one should be produced. Make sure to only use input values of the expected type
|
||||
as declared in the `Info` struct, and send values of the similarly declared
|
||||
appropriate return type. Failure to do so will may result in a panic and
|
||||
sadness.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Stream returns the single value that was generated and then closes.
|
||||
func (obj *FooFunc) Stream() error {
|
||||
defer close(obj.init.Output) // the sender closes
|
||||
var result string
|
||||
for {
|
||||
select {
|
||||
case input, ok := <-obj.init.Input:
|
||||
if !ok {
|
||||
return nil // can't output any more
|
||||
}
|
||||
|
||||
ix := input.Struct()["a"].Int()
|
||||
if ix < 0 {
|
||||
return fmt.Errorf("we can't deal with negatives")
|
||||
}
|
||||
|
||||
result = fmt.Sprintf("the input is: %d", ix)
|
||||
|
||||
case <-obj.closeChan:
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case obj.init.Output <- &types.StrValue{
|
||||
V: result,
|
||||
}:
|
||||
|
||||
case <-obj.closeChan:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, we read our inputs from the `input` channel, and write to the
|
||||
`output` channel. Our code is careful to never block or deadlock, and can always
|
||||
exit if a close signal is requested. It also cleans up after itself by closing
|
||||
the `output` channel when it is done using it. This is done easily with `defer`.
|
||||
If it notices that the `input` channel closes, then it knows that no more input
|
||||
values are coming and it can consider shutting down early.
|
||||
|
||||
## Further considerations
|
||||
|
||||
There is some additional information that any function author will need to know.
|
||||
Each issue is listed separately below!
|
||||
|
||||
### Function struct
|
||||
|
||||
Each function will implement methods as pointer receivers on a function struct.
|
||||
The naming convention for resources is that they end with a `Func` suffix.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
type FooFunc struct {
|
||||
init *interfaces.Init
|
||||
|
||||
// this space can be used if needed
|
||||
|
||||
closeChan chan struct{} // shutdown signal
|
||||
}
|
||||
```
|
||||
|
||||
### Function registration
|
||||
|
||||
All functions 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
|
||||
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.
|
||||
This is useful to prevent code duplication in higher level function scenarios.
|
||||
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 function that uses this feature, or to add it to an existing one!
|
||||
We don't expect this functionality to be particularly useful or common, as it's
|
||||
probably easier and preferable to simply import common golang library code into
|
||||
multiple different functions instead.
|
||||
|
||||
## Polymorphic Function API
|
||||
|
||||
The polymorphic function API is an API that lets you implement functions which
|
||||
do not necessarily have a single static function signature. After compile time,
|
||||
all functions must have a static function signature. We also know that there
|
||||
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 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
|
||||
existing implementations, and ask around in the community.
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
|
||||
### Can I use global variables?
|
||||
|
||||
Probably not. You must assume that multiple copies of your function may be used
|
||||
at the same time. If they require a global variable, it's likely this won't
|
||||
work. Instead it's probably better to use a struct local variable if you need to
|
||||
store some state.
|
||||
|
||||
There might be some rare instances where a global would be acceptable, but if
|
||||
you need one of these, you're probably already an internals expert. If you think
|
||||
they need to lock or synchronize so as to not overwhelm an external resource,
|
||||
then you have to be especially careful not to cause deadlocking the mgmt engine.
|
||||
|
||||
### Can I write functions in a different language?
|
||||
|
||||
Currently `golang` is the only supported language for built-in functions. We
|
||||
might consider allowing external functions to be imported in the future. This
|
||||
will likely require a language that can expose a C-like API, such as `python` or
|
||||
`ruby`. Custom `golang` functions are already possible when using mgmt as a lib.
|
||||
|
||||
### What new functions need writing?
|
||||
|
||||
There are still many ideas for new functions that haven't been written yet. If
|
||||
you'd like to contribute one, please contact us and tell us about your idea!
|
||||
|
||||
### Can I generate many different `FuncValue` implementations from one function?
|
||||
|
||||
Yes, you can use a function generator in `golang` to build multiple different
|
||||
implementations from the same function generator. You just need to implement a
|
||||
function which *returns* a `golang` type of `func([]types.Value) (types.Value, error)`
|
||||
which is what `FuncValue` expects. The generator function can use any input it
|
||||
wants to build the individual functions, thus helping with code re-use.
|
||||
|
||||
### How do I determine the signature of my simple, polymorphic function?
|
||||
|
||||
The determination of the input portion of the function signature can be
|
||||
determined by inspecting the length of the input, and the specific type each
|
||||
value has. Length is done in the standard `golang` way, and the type of each
|
||||
element can be ascertained with the `Type()` method available on every value.
|
||||
|
||||
Knowing the output type is trickier. If it can not be inferred in some manner,
|
||||
then the only way is to keep track of this yourself. You can use a function
|
||||
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).
|
||||
|
||||
## Suggestions
|
||||
|
||||
If you have any ideas for API changes or other improvements to function writing,
|
||||
please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in
|
||||
order to get it right!
|
||||
17
docs/index.rst
Normal file
17
docs/index.rst
Normal file
@@ -0,0 +1,17 @@
|
||||
.. mgmt documentation master file, created by
|
||||
sphinx-quickstart on Wed Feb 15 21:34:09 2017.
|
||||
You can adapt this file completely to your liking, but it should at least
|
||||
contain the root `toctree` directive.
|
||||
|
||||
Welcome to mgmt's documentation!
|
||||
================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
documentation
|
||||
quick-start-guide
|
||||
resource-guide
|
||||
prometheus
|
||||
puppet-guide
|
||||
913
docs/language-guide.md
Normal file
913
docs/language-guide.md
Normal file
@@ -0,0 +1,913 @@
|
||||
# Language guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `mgmt` tool has various frontends, each of which may produce a stream of
|
||||
between zero or more graphs that are passed to the engine for desired state
|
||||
application. In almost all scenarios, you're going to want to use the language
|
||||
frontend. This guide describes some of the internals of the language.
|
||||
|
||||
## Theory
|
||||
|
||||
The mgmt language is a declarative (immutable) functional, reactive programming
|
||||
language. It is implemented in `golang`. A longer introduction to the language
|
||||
is [available as a blog post here](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)!
|
||||
|
||||
### Types
|
||||
|
||||
All expressions must have a type. A composite type such as a list of strings
|
||||
(`[]str`) is different from a list of integers (`[]int`).
|
||||
|
||||
There _is_ a _variant_ type in the language's type system, but it is only used
|
||||
internally and only appears briefly when needed for type unification hints
|
||||
during static polymorphic function generation. This is an advanced topic which
|
||||
is not required for normal usage of the software.
|
||||
|
||||
The implementation of the internal types can be found in
|
||||
[lang/types/](https://github.com/purpleidea/mgmt/tree/master/lang/types/).
|
||||
|
||||
#### bool
|
||||
|
||||
A `true` or `false` value.
|
||||
|
||||
#### str
|
||||
|
||||
Any `"string!"` enclosed in quotes.
|
||||
|
||||
#### int
|
||||
|
||||
A number like `42` or `-13`. Integers are represented internally as golang's
|
||||
`int64`.
|
||||
|
||||
#### float
|
||||
|
||||
A floating point number like: `3.1415926`. Float's are represented internally as
|
||||
golang's `float64`.
|
||||
|
||||
#### list
|
||||
|
||||
An ordered collection of values of the same type, eg: `[6, 7, 8, 9,]`. It is
|
||||
worth mentioning that empty lists have a type, although without type hints it
|
||||
can be impossible to infer the item's type.
|
||||
|
||||
#### map
|
||||
|
||||
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,}`.
|
||||
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.
|
||||
|
||||
#### struct
|
||||
|
||||
An ordered set of field names and corresponding values, each of their own type,
|
||||
eg: `struct{answer => "42", james => "awesome", is_mgmt_awesome => true,}`.
|
||||
These are useful for combining more than one type into the same value. Note the
|
||||
syntactical difference between these and map's: the key's in map's have types,
|
||||
and as a result, string keys are enclosed in quotes, whereas struct _fields_ are
|
||||
not string values, and as such are bare and specified without quotes.
|
||||
|
||||
#### func
|
||||
|
||||
An ordered set of optionally named, differently typed input arguments, and a
|
||||
return type, eg: `func(s str) int` or:
|
||||
`func(bool, []str, {str: float}) struct{foo str; bar int}`.
|
||||
|
||||
### Expressions
|
||||
|
||||
Expressions, and the `Expr` interface need to be better documented. For now
|
||||
please consume
|
||||
[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go).
|
||||
These docs will be expanded on when things are more certain to be stable.
|
||||
|
||||
### Statements
|
||||
|
||||
There are a very small number of statements in our language. They include:
|
||||
|
||||
- **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
|
||||
|
||||
```mcl
|
||||
if <conditional> {
|
||||
<statements>
|
||||
} else {
|
||||
# the else branch is optional for if statements
|
||||
<statements>
|
||||
}
|
||||
```
|
||||
|
||||
- **resource**: produces a resource
|
||||
|
||||
```mcl
|
||||
file "/tmp/hello" {
|
||||
content => "world",
|
||||
mode => "o=rwx",
|
||||
}
|
||||
```
|
||||
|
||||
- **edge**: produces an edge
|
||||
|
||||
```mcl
|
||||
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
|
||||
of executing our programs is to produce a list of `resources`, which along with
|
||||
the produced `edges`, is built into a resource graph. This graph is then passed
|
||||
to the engine for desired state application.
|
||||
|
||||
#### Bind
|
||||
|
||||
This section needs better documentation.
|
||||
|
||||
#### If
|
||||
|
||||
This section needs better documentation.
|
||||
|
||||
#### Resource
|
||||
|
||||
Resources express the idempotent workloads that we want to have apply on our
|
||||
system. They correspond to vertices in a [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
|
||||
which represent the order in which their declared state is applied. You will
|
||||
usually want to pass in a number of parameters and associated values to the
|
||||
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
|
||||
ensure that the file has a length of zero. Alternatively you might wish to not
|
||||
specify the file contents at all, which would leave that property undefined. If
|
||||
you omit listing a property, then it will be undefined. To control this property
|
||||
programmatically, you need to specify an `is-defined` value, as well as the
|
||||
value to use if that boolean is true. You can do this with the resource-specific
|
||||
`elvis` operator.
|
||||
|
||||
```mcl
|
||||
$b = true # change me to false and then try editing the file manually
|
||||
file "/tmp/mgmt-elvis" {
|
||||
content => $b ?: "hello world\n",
|
||||
state => $const.res.file.state.exists,
|
||||
}
|
||||
```
|
||||
|
||||
This example is static, however you can imagine that the `$b` value might be
|
||||
chosen in a programmatic way, even one in which that value varies over time. If
|
||||
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
|
||||
edge dependencies, and the second two are normal edge dependencies that also
|
||||
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:
|
||||
|
||||
```mcl
|
||||
$b = true # for example purposes
|
||||
if $b {
|
||||
pkg "drbd" {
|
||||
state => "installed",
|
||||
|
||||
# multiple properties may be used in the same resource
|
||||
Before => File["/etc/drbd.conf"],
|
||||
Before => Svc["drbd"],
|
||||
}
|
||||
}
|
||||
file "/etc/drbd.conf" {
|
||||
content => "some config",
|
||||
|
||||
Depend => $b ?: Pkg["drbd"],
|
||||
Notify => Svc["drbd"],
|
||||
}
|
||||
svc "drbd" {
|
||||
state => "running",
|
||||
}
|
||||
```
|
||||
|
||||
There are two unique properties about these edges that is different from what
|
||||
you might expect from other automation software:
|
||||
|
||||
1. The ability to specify multiples of these properties allows you to avoid
|
||||
having to manage arrays and conditional trees of these different dependencies.
|
||||
2. The keywords all have the same length, which means your code lines up nicely.
|
||||
|
||||
#### Edge
|
||||
|
||||
Edges express dependencies in the graph of resources which are output. They can
|
||||
be chained as a pair, or in any greater number. For example, you may write:
|
||||
|
||||
```mcl
|
||||
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
|
||||
```
|
||||
|
||||
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:
|
||||
* [Lexing](#lexing)
|
||||
* [Parsing](#parsing)
|
||||
* [Interpolation](#interpolation)
|
||||
* [Scope propagation](#scope-propagation)
|
||||
* [Type unification](#type-unification)
|
||||
* [Function graph generation](#function-graph-generation)
|
||||
* [Function engine creation and validation](#function-engine-creation-and-validation)
|
||||
|
||||
All of the above needs to be done every time the source code changes. After this
|
||||
point, the [function engine runs](#function-engine-running-and-interpret) and
|
||||
produces events. On every event, we "[interpret](#function-engine-running-and-interpret)"
|
||||
which produces a resource graph. This series of resource graphs are passed
|
||||
to the engine as they are produced.
|
||||
|
||||
What follows are some notes about each step.
|
||||
|
||||
#### Lexing
|
||||
|
||||
Lexing is done using [nex](https://github.com/blynn/nex). It is a pure-golang
|
||||
implementation which is similar to _Lex_ or _Flex_, but which produces golang
|
||||
code instead of C. It integrates reasonably well with golang's _yacc_ which is
|
||||
used for parsing. The token definitions are in:
|
||||
[lang/lexer.nex](https://github.com/purpleidea/mgmt/tree/master/lang/lexer.nex).
|
||||
Lexing and parsing run together by calling the `LexParse` method.
|
||||
|
||||
#### Parsing
|
||||
|
||||
The parser used is golang's implementation of
|
||||
[yacc](https://godoc.org/golang.org/x/tools/cmd/goyacc). The documentation is
|
||||
quite abysmal, so it's helpful to rely on the documentation from standard yacc
|
||||
and trial and error. One small advantage yacc has over standard yacc is that it
|
||||
can produce error messages from examples. The best documentation is to examine
|
||||
the source. There is a short write up available [here](https://research.swtch.com/yyerror).
|
||||
The yacc file exists at:
|
||||
[lang/parser.y](https://github.com/purpleidea/mgmt/tree/master/lang/parser.y).
|
||||
Lexing and parsing run together by calling the `LexParse` method.
|
||||
|
||||
#### Interpolation
|
||||
|
||||
Interpolation is used to transform the AST (which was produced from lexing and
|
||||
parsing) into one which is either identical or different. It expands strings
|
||||
which might contain expressions to be interpolated (eg: `"the answer is: ${foo}"`)
|
||||
and can be used for other scenarios in which one statement or expression would
|
||||
be better represented by a larger AST. Most nodes in the AST simply return their
|
||||
own node address, and do not modify the AST.
|
||||
|
||||
#### Scope propagation
|
||||
|
||||
Scope propagation passes the parent scope (starting with the top-level, built-in
|
||||
scope) down through the AST. This is necessary so that children nodes can access
|
||||
variables in the scope if needed. Most AST node's simply pass on the scope
|
||||
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
|
||||
programmer to specify by annotation every type throughout their whole program
|
||||
so that each `Expr` node in the AST knows what to expect. Type annotation is
|
||||
allowed in situations when you want to explicitly specify a type, or when the
|
||||
compiler cannot deduce it, however, most of it can usually be inferred.
|
||||
|
||||
For type inferrence to work, each node in the AST implements a `Unify` method
|
||||
which is able to return a list of invariants that must hold true. This starts at
|
||||
the top most AST node, and gets called through to it's children to assemble a
|
||||
giant list of invariants. The invariants can take different forms. They can
|
||||
specify that a particular expression must have a particular type, or they can
|
||||
specify that two expressions must have the same types. More complex invariants
|
||||
allow you to specify relationships between different types and expressions.
|
||||
Furthermore, invariants can allow you to specify that only one invariant out of
|
||||
a set must hold true.
|
||||
|
||||
Once the list of invariants has been collected, they are run through an
|
||||
invariant solver. The solver can return either return successfully or with an
|
||||
error. If the solver returns successfully, it means that it has found a trivial
|
||||
mapping between every expression and it's corresponding type. At this point it
|
||||
is a simple task to run `SetType` on every expression so that the types are
|
||||
known. If the solver returns in error, it is usually due to one of two
|
||||
possibilities:
|
||||
|
||||
1. Ambiguity
|
||||
|
||||
The solver does not have enough information to make a definitive or
|
||||
unique determination about the expression to type mappings. The set of
|
||||
invariants is ambiguous, and we cannot continue. An error will be
|
||||
returned to the programmer. In this scenario the user will probably need
|
||||
to add a type annotation, possibly because of a design bug in the user's
|
||||
program.
|
||||
|
||||
2. Conflict
|
||||
|
||||
The solver has conflicting information that cannot be reconciled. In
|
||||
this situation an explicit conflict has been found. If two invariants
|
||||
are found which both expect a particular expression to have different
|
||||
types, then it is not possible to find a valid solution. This almost
|
||||
always happens if the user has made a type error in their program.
|
||||
|
||||
Only one solver currently exists, but it is possible to easily plug in an
|
||||
alternate implementation if someone more skilled in the art of solver design
|
||||
would like to propose a more logical or performant variant.
|
||||
|
||||
#### Function graph generation
|
||||
|
||||
At this point we have a fully type AST. The AST must now be transformed into a
|
||||
directed, acyclic graph (DAG) data structure that represents the flow of data as
|
||||
necessary for everything to be reactive. Note that this graph is *different*
|
||||
from the resource graph which is produced and sent to the engine. It is just a
|
||||
coincidence that both happen to be DAG's. (You don't freak out when you see a
|
||||
list data structure show up in more than one place, do you?)
|
||||
|
||||
To produce this graph, each node has a `Graph` method which it can call. This
|
||||
starts at the top most node, and is called down through the AST. The edges in
|
||||
the graphs must represent the individual expression values which are passed
|
||||
from node to node. The names of the edges must match the function type argument
|
||||
names which are used in the definition of the corresponding function. These
|
||||
corresponding functions must exist for each expression node and are produced by
|
||||
calling that expression's `Func` method. These are usually called by the
|
||||
function engine during function creation and validation.
|
||||
|
||||
#### Function engine creation and validation
|
||||
|
||||
Finally we have a graph of the data flows. The function engine must first
|
||||
initialize which creates references to each of the necessary function
|
||||
implementations, and gets information about each one. It then needs to be type
|
||||
checked to ensure that the data flows all correctly match what is expected. If
|
||||
you were to pass an `int` to a function expecting a `bool`, this would be a
|
||||
problem. If all goes well, the program should get run shortly.
|
||||
|
||||
#### Function engine running and interpret
|
||||
|
||||
At this point the function engine runs. It produces a stream of events which
|
||||
cause the `Output()` method of the top-level program to run, which produces the
|
||||
list of resources and edges. These are then transformed into the resource graph
|
||||
which is passed to the engine.
|
||||
|
||||
### Function API
|
||||
|
||||
If you'd like to create a built-in, core function, you'll need to implement the
|
||||
function API interface named `Func`. It can be found in
|
||||
[lang/interfaces/func.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/func.go).
|
||||
Your function must have a specific type. For example, a simple math function
|
||||
might have a signature of `func(x int, y int) int`. As you can see, all the
|
||||
types are known _before_ compile time.
|
||||
|
||||
A separate discussion on this matter can be found in the [function guide](function-guide.md).
|
||||
|
||||
What follows are each of the method signatures and a description of each.
|
||||
Failure to implement the API correctly can cause the function graph engine to
|
||||
block, or the program to panic.
|
||||
|
||||
### Info
|
||||
|
||||
```golang
|
||||
Info() *Info
|
||||
```
|
||||
|
||||
The Info method must return a struct containing some information about your
|
||||
function. The struct has the following type:
|
||||
|
||||
```golang
|
||||
type Info struct {
|
||||
Sig *types.Type // the signature of the function, must be KindFunc
|
||||
}
|
||||
```
|
||||
|
||||
You must implement this correctly. Other fields in the `Info` struct may be
|
||||
added in the future. This method is usually called before any other, and should
|
||||
not depend on any other method being called first. Other methods must not depend
|
||||
on this method being called first.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
func (obj *FooFunc) Info() *interfaces.Info {
|
||||
return &interfaces.Info{
|
||||
Sig: types.NewType("func(a str, b int) float"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Init
|
||||
|
||||
```golang
|
||||
Init(*Init) error
|
||||
```
|
||||
|
||||
Init is called by the function graph engine to create an implementation of this
|
||||
function. It is passed in a struct of the following form:
|
||||
|
||||
```golang
|
||||
type Init struct {
|
||||
Hostname string // uuid for the host
|
||||
Input chan types.Value // Engine will close `input` chan
|
||||
Output chan types.Value // Stream must close `output` chan
|
||||
World resources.World
|
||||
Debug bool
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
```
|
||||
|
||||
These values and references may be used (wisely) inside your function. `Input`
|
||||
will contain a channel of input structs matching the expected input signature
|
||||
for your function. `Output` will be the channel which you must send values to
|
||||
whenever a new value should be produced. This must be done in the `Stream()`
|
||||
function. You may carefully use `World` to access functionality provided by the
|
||||
engine. You may use `Logf` to log informational messages, however there is no
|
||||
guarantee that they will be displayed to the user. `Debug` specifies whether the
|
||||
function is running in a user-requested debug mode. This might cause you to want
|
||||
to print more log messages for example. You will need to save references to any
|
||||
or all of these info fields that you wish to use in the struct implementing this
|
||||
`Func` interface. At a minimum you will need to save `Output` as a minimum of
|
||||
one value must be produced.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
Please see the example functions in
|
||||
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||
```
|
||||
|
||||
### Stream
|
||||
|
||||
```golang
|
||||
Stream() error
|
||||
```
|
||||
|
||||
Stream is called by the function engine when it is ready for your function to
|
||||
start accepting input and producing output. You must always produce at least one
|
||||
value. Failure to produce at least one value will probably cause the function
|
||||
engine to hang waiting for your output. This function must close the `Output`
|
||||
channel when it has no more values to send. The engine will close the `Input`
|
||||
channel when it has no more values to send. This may or may not influence
|
||||
whether or not you close the `Output` channel.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
Please see the example functions in
|
||||
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||
```
|
||||
|
||||
### Close
|
||||
|
||||
```golang
|
||||
Close() error
|
||||
```
|
||||
|
||||
Close asks the particular function to shutdown its `Stream()` function and
|
||||
return.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
Please see the example functions in
|
||||
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
|
||||
```
|
||||
|
||||
### Polymorphic Function API
|
||||
|
||||
For some functions, it might be helpful to be able to implement a function once,
|
||||
but to have multiple polymorphic variants that can be chosen at compile time.
|
||||
For this more advanced topic, you will need to use the
|
||||
[Polymorphic Function API](#polymorphic-function-api). This will help with code
|
||||
reuse when you have a small, finite number of possible type signatures, and also
|
||||
for more complicated cases where you might have an infinite number of possible
|
||||
type signatures. (eg: `[]str`, or `[][]str`, or `[][][]str`, etc...)
|
||||
|
||||
Suppose you want to implement a function which can assume different type
|
||||
signatures. The mgmt language does not support polymorphic types-- you must use
|
||||
static types throughout the language, however, it is legal to implement a
|
||||
function which can take different specific type signatures based on how it is
|
||||
used. For example, you might wish to add a math function which could take the
|
||||
form of `func(x int, x int) int` or `func(x float, x float) float` depending on
|
||||
the input values. You might also want to implement a function which takes an
|
||||
arbitrary number of input arguments (the number must be statically fixed at the
|
||||
compile time of your program though) and which returns a string.
|
||||
|
||||
The `PolyFunc` interface adds additional methods which you must implement to
|
||||
satisfy such a function implementation. If you'd like to implement such a
|
||||
function, then please notify the project authors, and they will expand this
|
||||
section with a longer description of the process.
|
||||
|
||||
#### Examples
|
||||
|
||||
What follows are a few examples that might help you understand some of the
|
||||
language details.
|
||||
|
||||
##### Example Foo
|
||||
|
||||
TODO: please add an example here!
|
||||
|
||||
##### Example Bar
|
||||
|
||||
TODO: please add an example here!
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
|
||||
### What is the difference between `ExprIf` and `StmtIf`?
|
||||
|
||||
The language contains both an `if` expression, and and `if` statement. An `if`
|
||||
expression takes a boolean conditional *and* it must contain exactly _two_
|
||||
branches (a `then` and an `else` branch) which each contain one expression. The
|
||||
`if` expression _will_ return the value of one of the two branches based on the
|
||||
conditional.
|
||||
|
||||
#### Example:
|
||||
|
||||
```mcl
|
||||
# this is an if expression, and both branches must exist
|
||||
$b = true
|
||||
$x = if $b {
|
||||
42
|
||||
} else {
|
||||
-13
|
||||
}
|
||||
```
|
||||
|
||||
The `if` statement also takes a boolean conditional, but it may have either one
|
||||
or two branches. Branches must only directly contain statements. The `if`
|
||||
statement does not return any value, but it does produce output when it is
|
||||
evaluated. The output consists primarily of resources (vertices) and edges.
|
||||
|
||||
#### Example:
|
||||
|
||||
```mcl
|
||||
# this is an if statement, and in this scenario the else branch was omitted
|
||||
$b = true
|
||||
if $b {
|
||||
file "/tmp/hello" {
|
||||
content => "world",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### What is the difference `types.Value.Str()` and `types.Value.String()`?
|
||||
|
||||
In the `lang/types` library, there is a `types.Value` interface. Every value in
|
||||
our type system must implement this interface. One of the methods in this
|
||||
interface is the `String() string` method. This lets you print a representation
|
||||
of the value. You will probably never need to use this method.
|
||||
|
||||
In addition, the `types.Value` interface implements a number of helper functions
|
||||
which return the value as an equivalent golang type. If you know that the value
|
||||
is a `bool`, you can call `x.Bool()` on it. If it's a `string` you can call
|
||||
`x.Str()`. Make sure not to call one of those type methods unless you know the
|
||||
value is of that type, or you will trigger a panic!
|
||||
|
||||
### I created a `&ListValue{}` but it's not working!
|
||||
|
||||
If you create a base type like `bool`, `str`, `int`, or `float`, all you need to
|
||||
do is build the `&BoolValue` and set the `V` field. Eg:
|
||||
|
||||
```golang
|
||||
someBool := &types.BoolValue{V: true}
|
||||
```
|
||||
|
||||
If you are building a container type like `list`, `map`, `struct`, or `func`,
|
||||
then you *also* need to specify the type of the contained values. This is
|
||||
because a list has a type of `[]str`, or `[]int`, or even `[][]foo`. Eg:
|
||||
|
||||
```golang
|
||||
someListOfStrings := &types.ListValue{
|
||||
T: types.NewType("[]str"), # must match the contents!
|
||||
V: []types.Value{
|
||||
&types.StrValue{V: "a"},
|
||||
&types.StrValue{V: "bb"},
|
||||
&types.StrValue{V: "ccc"},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
of graphs to the engine "backend". While it _is_ the recommended way of using
|
||||
mgmt, you're welcome to either use an alternate frontend, or write your own. To
|
||||
write your own frontend, you must implement the
|
||||
[GAPI](https://github.com/purpleidea/mgmt/blob/master/gapi/gapi.go) interface.
|
||||
|
||||
### I'm an expert in FRP, and you got it all wrong; even the names of things!
|
||||
|
||||
I am certainly no expert in FRP, and I've certainly got lots more to learn. One
|
||||
thing FRP experts might notice is that some of the concepts from FRP are either
|
||||
named differently, or are notably absent.
|
||||
|
||||
In mgmt, we don't talk about behaviours, events, or signals in the strict FRP
|
||||
definitons of the words. Firstly, because we only support discretized, streams
|
||||
of values with no plan to add continuous semantics. Secondly, because we prefer
|
||||
to use terms which are more natural and relatable to what our target audience is
|
||||
expecting. Our users are more likely to have a background in Physiology, or
|
||||
systems administration than a background in FRP.
|
||||
|
||||
Having said that, we hope that the FRP community will engage with us and help
|
||||
improve the parts that we got wrong. Even if that means adding continuous
|
||||
behaviours!
|
||||
|
||||
### This is brilliant, may I give you a high-five?
|
||||
|
||||
Thank you, and yes, probably. "Props" may also be accepted, although patches are
|
||||
preferred. If you can't do either, [donations](https://purpleidea.com/misc/donate/)
|
||||
to support the project are welcome too!
|
||||
|
||||
### 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).
|
||||
|
||||
## Suggestions
|
||||
|
||||
If you have any ideas for changes or other improvements to the language, please
|
||||
let us know! We're still pre 1.0 and pre 0.1 and happy to change it in order to
|
||||
get it right!
|
||||
59
docs/on-the-web.md
Normal file
59
docs/on-the-web.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# On the web
|
||||
|
||||
Here is a list of places mgmt has appeared on the web. Feel free to send a patch
|
||||
if we missed something that you think is relevant!
|
||||
|
||||
## Links
|
||||
|
||||
| Author | Format | Subject |
|
||||
|---|---|---|
|
||||
| James Shubin | blog | [Next generation configuration mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) |
|
||||
| James Shubin | video | [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1) |
|
||||
| James Shubin | video | [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1) |
|
||||
| Julian Dunn | video | [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1) |
|
||||
| Walter Heck | slides | [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3) |
|
||||
| Marco Marongiu | blog | [On mgmt](http://syslog.me/2016/02/15/leap-or-die/) |
|
||||
| Felix Frank | blog | [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/) |
|
||||
| James Shubin | blog | [Automatic edges in mgmt (...and the pkg resource)](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/) |
|
||||
| James Shubin | blog | [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/) |
|
||||
| John Arundel | tweet | [“Puppet’s days are numbered.”](https://twitter.com/bitfield/status/732157519142002688) |
|
||||
| Felix Frank | blog | [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/) |
|
||||
| Felix Frank | blog | [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/) |
|
||||
| James Shubin | blog | [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) |
|
||||
| James Shubin | video | [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1) |
|
||||
| James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf)) |
|
||||
| Felix Frank | blog | [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/) |
|
||||
| Felix Frank | blog | [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/) |
|
||||
| James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1) |
|
||||
| James Shubin | blog | [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) |
|
||||
| James Shubin | video | [Recording from High Load Strategy 2016](https://vimeo.com/191493409) |
|
||||
| James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1) |
|
||||
| James Shubin | blog | [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/) |
|
||||
| Julien Pivotto | blog | [Augeas resource for mgmt](https://roidelapluie.be/blog/2017/02/14/mgmt-augeas/) |
|
||||
| James Shubin | blog | [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/) |
|
||||
| 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](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) |
|
||||
| James Shubin | video | [Recording from LCA 2018, Main Conference](https://www.youtube.com/watch?v=_9PG64AOQ3w) |
|
||||
| James Shubin | video | [Recording from DevConf.cz 2017](https://www.youtube.com/watch?v=-FPEK08l1Zk) |
|
||||
| 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) |
|
||||
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2023](https://www.youtube.com/watch?v=FeRGRj8w0BU) |
|
||||
66
docs/prometheus.md
Normal file
66
docs/prometheus.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Prometheus support
|
||||
|
||||
Mgmt comes with a built-in prometheus support. It is disabled by default, and
|
||||
can be enabled with the `--prometheus` command line switch.
|
||||
|
||||
By default, the prometheus instance will listen on [`127.0.0.1:9233`][pd]. You
|
||||
can change this setting by using the `--prometheus-listen` cli option:
|
||||
|
||||
To have mgmt prometheus bind interface on 0.0.0.0:45001, use:
|
||||
`./mgmt r --prometheus --prometheus-listen :45001`
|
||||
|
||||
## Metrics
|
||||
|
||||
Mgmt exposes three kinds of resources: _go_ metrics, _etcd_ metrics and _mgmt_
|
||||
metrics.
|
||||
|
||||
### go metrics
|
||||
|
||||
We use the [prometheus go_collector][pgc] to expose go metrics. Those metrics
|
||||
are mainly useful for debugging and perf testing.
|
||||
|
||||
### etcd metrics
|
||||
|
||||
mgmt exposes etcd metrics. Read more in the [upstream documentation][etcdm]
|
||||
|
||||
### mgmt metrics
|
||||
|
||||
Here is a list of the metrics we provide:
|
||||
|
||||
- `mgmt_resources_total`: The number of resources that mgmt is managing
|
||||
- `mgmt_checkapply_total`: The number of CheckApply's that mgmt has run
|
||||
- `mgmt_failures_total`: The number of resources that have failed
|
||||
- `mgmt_failures`: The number of resources that have failed
|
||||
- `mgmt_graph_start_time_seconds`: Start time of the current graph since unix
|
||||
epoch in seconds
|
||||
|
||||
For each metric, you will get some extra labels:
|
||||
|
||||
- `kind`: The kind of mgmt resource
|
||||
|
||||
For `mgmt_checkapply_total`, those extra labels are set:
|
||||
|
||||
- `eventful`: "true" or "false", if the CheckApply triggered some changes
|
||||
- `errorful`: "true" or "false", if the CheckApply reported an error
|
||||
- `apply`: "true" or "false", if the CheckApply ran in apply or noop mode
|
||||
|
||||
## Alerting
|
||||
|
||||
You can use prometheus to alert you upon changes or failures. We do not provide
|
||||
such templates yet, but we plan to provide some examples in this repository.
|
||||
Patches welcome!
|
||||
|
||||
## Grafana
|
||||
|
||||
We do not have grafana dashboards yet. Patches welcome!
|
||||
|
||||
## External resources
|
||||
|
||||
- [prometheus website](https://prometheus.io/)
|
||||
- [prometheus documentation](https://prometheus.io/docs/introduction/overview/)
|
||||
- [prometheus best practices regarding metrics naming](https://prometheus.io/docs/practices/naming/)
|
||||
- [grafana website](http://grafana.org/)
|
||||
|
||||
[pgc]: https://github.com/prometheus/client_golang/blob/master/prometheus/go_collector.go
|
||||
[etcdm]: https://coreos.com/etcd/docs/latest/metrics.html
|
||||
[pd]: https://github.com/prometheus/prometheus/wiki/Default-port-allocations
|
||||
@@ -1,22 +1,13 @@
|
||||
#mgmt Puppet support
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
* [Testing the Puppet side](#testing-the-puppet-side)
|
||||
2. [Writing a suitable manifest](#writing-a-suitable-manifest)
|
||||
* [Unsupported attributes](#unsupported-attributes)
|
||||
* [Unsupported resources](#unsupported-resources)
|
||||
* [Avoiding common warnings](#avoiding-common-warnings)
|
||||
3. [Configuring Puppet](#configuring-puppet)
|
||||
4. [Caveats](#caveats)
|
||||
# Puppet guide
|
||||
|
||||
`mgmt` can use Puppet as its source for the configuration graph.
|
||||
This document goes into detail on how this works, and lists
|
||||
some pitfalls and limitations.
|
||||
|
||||
For basic instructions on how to use the Puppet support, see
|
||||
the [main documentation](DOCUMENTATION.md#puppet-support).
|
||||
the [main documentation](documentation.md#puppet-support).
|
||||
|
||||
##Prerequisites
|
||||
## Prerequisites
|
||||
|
||||
You need Puppet installed in your system. It is not important how you
|
||||
get it. On the most common Linux distributions, you can use packages
|
||||
@@ -29,14 +20,16 @@ Any release of Puppet's 3.x and 4.x series should be suitable for use with
|
||||
`mgmt`. Most importantly, make sure to install the `ffrank-mgmtgraph` Puppet
|
||||
module (referred to below as "the translator module").
|
||||
|
||||
puppet module install ffrank-mgmtgraph
|
||||
```
|
||||
puppet module install ffrank-mgmtgraph
|
||||
```
|
||||
|
||||
Please note that the module is not required on your Puppet master (if you
|
||||
use a master/agent setup). It's needed on the machine that runs `mgmt`.
|
||||
You can install the module on the master anyway, so that it gets distributed
|
||||
to your agents through Puppet's `pluginsync` mechanism.
|
||||
|
||||
###Testing the Puppet side
|
||||
### Testing the Puppet side
|
||||
|
||||
The following command should run successfully and print a YAML hash on your
|
||||
terminal:
|
||||
@@ -48,9 +41,9 @@ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": ensure => present }'
|
||||
You can use this CLI to test any manifests before handing them straight
|
||||
to `mgmt`.
|
||||
|
||||
##Writing a suitable manifest
|
||||
## Writing a suitable manifest
|
||||
|
||||
###Unsupported attributes
|
||||
### Unsupported attributes
|
||||
|
||||
`mgmt` inherited its resource module from Puppet, so by and large, it's quite
|
||||
possible to express `mgmt` graphs in terms of Puppet manifests. However,
|
||||
@@ -62,8 +55,10 @@ For example, at the time of writing this, the `file` type in `mgmt` had no
|
||||
notion of permissions (the file `mode`) yet. This lead to the following
|
||||
warning (among others that will be discussed below):
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/foo": mode => "0600" }'
|
||||
Warning: cannot translate: File[/tmp/foo] { mode => "600" } (attribute is ignored)
|
||||
```
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/foo": mode => "0600" }'
|
||||
Warning: cannot translate: File[/tmp/foo] { mode => "600" } (attribute is ignored)
|
||||
```
|
||||
|
||||
This is a heads-up for the user, because the resulting `mgmt` graph will
|
||||
in fact not pass this information to the `/tmp/foo` file resource, and
|
||||
@@ -71,7 +66,7 @@ in fact not pass this information to the `/tmp/foo` file resource, and
|
||||
manifests that are written expressly for `mgmt` is not sensible and should
|
||||
be avoided.
|
||||
|
||||
###Unsupported resources
|
||||
### Unsupported resources
|
||||
|
||||
Puppet has a fairly large number of
|
||||
[built-in types](https://docs.puppet.com/puppet/latest/reference/type.html),
|
||||
@@ -91,28 +86,32 @@ this overhead can amount to several orders of magnitude.
|
||||
|
||||
Avoid Puppet types that `mgmt` does not implement (yet).
|
||||
|
||||
###Avoiding common warnings
|
||||
### Avoiding common warnings
|
||||
|
||||
Many resource parameters in Puppet take default values. For the most part,
|
||||
the translator module just ignores them. However, there are cases in which
|
||||
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
|
||||
For example, translating a plain `file` resource will lead to a warning message:
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": }'
|
||||
Warning: File[/tmp/mgmt-test] uses the 'puppet' file bucket, which mgmt cannot do. There will be no backup copies!
|
||||
```
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": }'
|
||||
Warning: File[/tmp/mgmt-test] uses the 'puppet' file bucket, which mgmt cannot do. There will be no backup copies!
|
||||
```
|
||||
|
||||
The reason is that per default, Puppet assumes the following parameter value
|
||||
(among others)
|
||||
|
||||
```puppet
|
||||
file { "/tmp/mgmt-test":
|
||||
backup => 'puppet',
|
||||
backup => 'puppet',
|
||||
}
|
||||
```
|
||||
|
||||
To avoid this, specify the parameter explicitly:
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
|
||||
```bash
|
||||
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
|
||||
```
|
||||
|
||||
This is tedious in a more complex manifest. A good simplification is the
|
||||
following [resource default](https://docs.puppet.com/puppet/latest/reference/lang_defaults.html)
|
||||
@@ -125,7 +124,7 @@ File { backup => false }
|
||||
If you encounter similar warnings from other types and/or parameters,
|
||||
use the same approach to silence them if possible.
|
||||
|
||||
##Configuring Puppet
|
||||
## Configuring Puppet
|
||||
|
||||
Since `mgmt` uses an actual Puppet CLI behind the scenes, you might
|
||||
need to tweak some of Puppet's runtime options in order to make it
|
||||
@@ -143,16 +142,20 @@ control all of them, through its `--puppet-conf` option. It allows
|
||||
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
|
||||
`[main]` section:
|
||||
|
||||
[main]
|
||||
server=mgmt-master.example.net
|
||||
vardir=/var/lib/mgmt/puppet
|
||||
```
|
||||
[main]
|
||||
server=mgmt-master.example.net
|
||||
vardir=/var/lib/mgmt/puppet
|
||||
```
|
||||
|
||||
##Caveats
|
||||
## Caveats
|
||||
|
||||
Please see the [README](https://github.com/ffrank/puppet-mgmtgraph/blob/master/README.md)
|
||||
of the translator module for the current state of supported and unsupported
|
||||
@@ -161,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.
|
||||
111
docs/quick-start-guide.md
Normal file
111
docs/quick-start-guide.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Quick start guide
|
||||
|
||||
## Introduction
|
||||
|
||||
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...
|
||||
|
||||
## Getting mgmt
|
||||
|
||||
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.
|
||||
|
||||
### 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.18 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 too old, you may need to [download](https://golang.org/dl/)
|
||||
a newer golang version.
|
||||
|
||||
#### Setting up golang
|
||||
|
||||
* 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).
|
||||
|
||||
#### Getting the mgmt code and associated dependencies
|
||||
|
||||
* Download the `mgmt` code and switch to that directory:
|
||||
|
||||
```shell
|
||||
git clone --recursive https://github.com/purpleidea/mgmt/ ~/mgmt/
|
||||
cd ~/mgmt/
|
||||
```
|
||||
|
||||
* 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` if you want to see the details of what it does.
|
||||
|
||||
#### Building mgmt
|
||||
|
||||
* 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! You can press `^C` to exit `mgmt`.
|
||||
* Have fun hacking on our future technology and get involved to shape the
|
||||
project!
|
||||
|
||||
## Examples
|
||||
|
||||
Please look in the [examples/lang/](../examples/lang/) folder for some more
|
||||
examples!
|
||||
798
docs/resource-guide.md
Normal file
798
docs/resource-guide.md
Normal file
@@ -0,0 +1,798 @@
|
||||
# Resource guide
|
||||
|
||||
## Overview
|
||||
|
||||
The `mgmt` tool has built-in resource primitives which make up the building
|
||||
blocks of any configuration. Each instance of a resource is mapped to a single
|
||||
vertex in the resource [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
|
||||
This guide is meant to instruct developers on how to write a brand new resource.
|
||||
Since `mgmt` and the core resources are written in golang, some prior golang
|
||||
knowledge is assumed.
|
||||
|
||||
## Theory
|
||||
|
||||
Resources in `mgmt` are similar to resources in other systems in that they are
|
||||
[idempotent](https://en.wikipedia.org/wiki/Idempotence). Our resources are
|
||||
uniquely different in that they can detect when their state has changed, and as
|
||||
a result can run to revert or repair this change instantly. For some background
|
||||
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/engine/resources.go)
|
||||
interface. What follows are each of the method signatures and a description of
|
||||
each.
|
||||
|
||||
### Default
|
||||
|
||||
```golang
|
||||
Default() engine.Res
|
||||
```
|
||||
|
||||
This returns a populated resource struct as a `Res`. It shouldn't populate any
|
||||
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() engine.Res {
|
||||
return &FooRes{
|
||||
Answer: 42, // sometimes, defaults shouldn't be the zero value
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Validate
|
||||
|
||||
```golang
|
||||
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 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 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
|
||||
|
||||
```golang
|
||||
// Validate reports any problems with the struct definition.
|
||||
func (obj *FooRes) Validate() error {
|
||||
if obj.Answer != 42 { // validate whatever you want
|
||||
return fmt.Errorf("expected an answer of 42")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Init
|
||||
|
||||
```golang
|
||||
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 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(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 nil
|
||||
}
|
||||
```
|
||||
|
||||
This method is always called after `Validate` has run successfully, with the
|
||||
exception that we can't prevent a malicious or buggy `libmgmt` user to not run
|
||||
this. In other words, you should expect `Validate` to have run first, but you
|
||||
shouldn't allow `Init` to dangerously `rm -rf /$the_world` if your code only
|
||||
checks `$the_world` in `Validate`. Remember to always program safely!
|
||||
|
||||
### Close
|
||||
|
||||
```golang
|
||||
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. It is *not*
|
||||
the shutdown signal that tells the resource to exit. That happens in the Watch
|
||||
loop.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Close runs some cleanup code for this resource.
|
||||
func (obj *FooRes) Close() error {
|
||||
err := obj.conn.Close() // close some internal connection
|
||||
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.
|
||||
|
||||
### CheckApply
|
||||
|
||||
```golang
|
||||
CheckApply(apply bool) (checkOK bool, err error)
|
||||
```
|
||||
|
||||
`CheckApply` is where the real _work_ is done. Under normal circumstances, this
|
||||
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 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
|
||||
that changes can't be made because `apply` is `false`, you should then return
|
||||
`(false, nil)`.
|
||||
|
||||
You must cause the resource to converge during a single execution of this
|
||||
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 new change, ultimately resulting in a subsequent call
|
||||
to `CheckApply`.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// CheckApply does the idempotent work of checking and applying resource state.
|
||||
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, 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
|
||||
}
|
||||
```
|
||||
|
||||
The `CheckApply` function is called by the `mgmt` engine when it believes a call
|
||||
is necessary. Under certain conditions when a `Watch` call does not invalidate
|
||||
the state of the resource, and no refresh call was sent, its execution might be
|
||||
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.
|
||||
|
||||
#### Paired execution
|
||||
|
||||
For many resources it is not uncommon to see `CheckApply` run twice in rapid
|
||||
succession. This is usually not a pathological occurrence, but rather a healthy
|
||||
pattern which is a consequence of the event system. When the state of the
|
||||
resource is incorrect, `CheckApply` will run to remedy the state. In response to
|
||||
having just changed the state, it is usually the case that this repair will
|
||||
trigger the `Watch` code! In response, a second `CheckApply` is triggered, which
|
||||
will likely find the state to now be correct.
|
||||
|
||||
#### Summary
|
||||
|
||||
* Anytime an error occurs during `CheckApply`, you should return `(false, err)`.
|
||||
* 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 can have a negative effect.
|
||||
|
||||
### Watch
|
||||
|
||||
```golang
|
||||
Watch() error
|
||||
```
|
||||
|
||||
`Watch` is a main loop that runs and sends messages when it detects that the
|
||||
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 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
|
||||
to be re-engineered so that this isn't the case. If you have an idea for a
|
||||
resource which would fit this criteria, but you can't find a solution, please
|
||||
contact the `mgmt` maintainers so that this problem can be investigated and a
|
||||
possible system level engineering fix can be found.
|
||||
|
||||
You may have trouble deciding how much resource state checking should happen in
|
||||
the `Watch` loop versus deferring it all to the `CheckApply` method. You may
|
||||
want to put some simple fast path checking in `Watch` to avoid generating
|
||||
obviously spurious events, but in general it's best to keep the `Watch` method
|
||||
as simple as possible. Contact the `mgmt` maintainers if you're not sure.
|
||||
|
||||
If the resource is activated in `polling` mode, the `Watch` method will not get
|
||||
executed. As a result, the resource must still work even if the main loop is not
|
||||
running.
|
||||
|
||||
#### Select
|
||||
|
||||
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 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 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
|
||||
|
||||
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. 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.
|
||||
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
|
||||
sending out erroneous `Event` messages to keep things alive until it finishes.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// Watch is the listener and main loop for this resource.
|
||||
func (obj *FooRes) Watch() error {
|
||||
// setup the Foo resource
|
||||
var err error
|
||||
if err, obj.foo = OpenFoo(); err != nil {
|
||||
return err // we couldn't startup
|
||||
}
|
||||
defer obj.whatever.CloseFoo() // shutdown our Foo
|
||||
|
||||
// notify engine that we're running
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
// the actual events!
|
||||
case event := <-obj.foo.Events:
|
||||
if is_an_event {
|
||||
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.init.Event()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Summary
|
||||
|
||||
* 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.
|
||||
|
||||
### Cmp
|
||||
|
||||
```golang
|
||||
Cmp(engine.Res) error
|
||||
```
|
||||
|
||||
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
|
||||
// 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 fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Whatever != res.Whatever {
|
||||
return fmt.Errorf("the Whatever param differs")
|
||||
}
|
||||
if obj.Flag != res.Flag {
|
||||
return fmt.Errorf("the Flag param differs")
|
||||
}
|
||||
|
||||
return nil // they must match!
|
||||
}
|
||||
```
|
||||
|
||||
## 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() []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
|
||||
|
||||
```golang
|
||||
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.
|
||||
|
||||
### Groupable
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
This is optional, but recommended for any resource that will have a YAML
|
||||
accessible struct. It is not required because to do so would mean that
|
||||
third-party or custom resources (such as those someone writes to use with
|
||||
`libmgmt`) would have to implement this needlessly.
|
||||
|
||||
The signature intentionally matches what is required to satisfy the `go-yaml`
|
||||
[Unmarshaler](https://godoc.org/gopkg.in/yaml.v2#Unmarshaler) interface.
|
||||
|
||||
#### Example
|
||||
|
||||
```golang
|
||||
// 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
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*FooRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to FooRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = FooRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
## 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/).
|
||||
When using this feature, the engine will automatically send the user specified
|
||||
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 `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.init.Recv()["SomeKey"]; exists {
|
||||
obj.init.Logf("the SomeKey param was sent to us from: %s.%s", val.Res, val.Key)
|
||||
if val.Changed {
|
||||
obj.init.Logf("the SomeKey param was just updated!")
|
||||
// you may want to invalidate some local cache
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The specifics of resource sending are not currently documented. Please send a
|
||||
patch here!
|
||||
|
||||
## Composite resources
|
||||
|
||||
Composite resources are resources which embed one or more existing resources.
|
||||
This is useful to prevent code duplication in higher level resource scenarios.
|
||||
The best example of this technique can be seen in the `nspawn` resource which
|
||||
can be seen to partially embed a `svc` resource, but without its `Watch`.
|
||||
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!
|
||||
|
||||
## Frequently asked questions
|
||||
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
|
||||
### Can I write resources in a different language?
|
||||
|
||||
Currently `golang` is the only supported language for built-in resources. We
|
||||
might consider allowing external resources to be imported in the future. This
|
||||
will likely require a language that can expose a C-like API, such as `python` or
|
||||
`ruby`. Custom `golang` resources are already possible when using mgmt as a lib.
|
||||
Higher level resource collections will be possible once the `mgmt` DSL is ready.
|
||||
|
||||
### Why does the resource API have `CheckApply` instead of two separate methods?
|
||||
|
||||
In an early version we actually had both "parts" as separate methods, namely:
|
||||
`StateOK` (Check) and `Apply`, but the [decision](58f41eddd9c06b183f889f15d7c97af81b0331cc)
|
||||
was made to merge the two into a single method. There are two reasons for this:
|
||||
|
||||
1. Many situations would involve the engine running both `Check` and `Apply`. If
|
||||
the resource needed to share some state (for efficiency purposes) between the
|
||||
two calls, this is much more difficult. A common example is that a resource
|
||||
might want to open a connection to `dbus` or `http` to do resource state testing
|
||||
and applying. If the methods are combined, there's no need to open and close
|
||||
them twice. A counter argument might be that you could open the connection in
|
||||
`Init`, and close it in `Close`, however you might not want that open for the
|
||||
full lifetime of the resource if you only change state occasionally.
|
||||
2. Suppose you came up with a really good reason why you wanted the two methods
|
||||
to be separate. It turns out that the current `CheckApply` can wrap this easily.
|
||||
It would look approximately like this:
|
||||
|
||||
```golang
|
||||
func (obj *FooRes) CheckApply(apply bool) (bool, error) {
|
||||
// my private split implementation of check and apply
|
||||
if c, err := obj.check(); err != nil {
|
||||
return false, err // we errored
|
||||
} else if c {
|
||||
return true, nil // state was good!
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil // state needs fixing, but apply is false
|
||||
}
|
||||
|
||||
err := obj.apply() // errors if failure or unable to apply
|
||||
|
||||
return false, err // always return false, with an optional error
|
||||
}
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
## Suggestions
|
||||
|
||||
If you have any ideas for API changes or other improvements to resource writing,
|
||||
please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in
|
||||
order to get it right!
|
||||
250
docs/resources.md
Normal file
250
docs/resources.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# Resources
|
||||
|
||||
Here we list all the built-in resources and their properties. The resource
|
||||
primitives in `mgmt` are typically more powerful than resources in other
|
||||
configuration management systems because they can be event based which lets them
|
||||
respond in real-time to converge to the desired state. This property allows you
|
||||
to build more complex resources that you probably hadn't considered in the past.
|
||||
|
||||
In addition to the resource specific properties, there are resource properties
|
||||
(otherwise known as parameters) which can apply to every resource. These are
|
||||
called [meta parameters](documentation.md#meta-parameters) and are listed
|
||||
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/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
|
||||
|
||||
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.
|
||||
|
||||
## File
|
||||
|
||||
The file resource manages files and directories. In `mgmt`, directories are
|
||||
identified by a trailing slash in their path name. File have no such slash.
|
||||
|
||||
It has the following properties:
|
||||
|
||||
* `path`: absolute file path (directories have a trailing slash here)
|
||||
* `state`: either `exists`, `absent`, or undefined
|
||||
* `content`: raw file content
|
||||
* `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
|
||||
|
||||
### Path
|
||||
|
||||
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.
|
||||
|
||||
### Source
|
||||
|
||||
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.
|
||||
|
||||
### Fragments
|
||||
|
||||
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
|
||||
|
||||
The recurse property limits whether file resource operations should recurse into
|
||||
and monitor directory contents with a depth greater than one.
|
||||
|
||||
### Force
|
||||
|
||||
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
|
||||
on the system and watches them for changes.
|
||||
|
||||
### static_hostname
|
||||
|
||||
The static hostname 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.
|
||||
|
||||
### transient_hostname
|
||||
|
||||
The transient / dynamic hostname 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.
|
||||
|
||||
### pretty_hostname
|
||||
|
||||
The pretty hostname is a free-form UTF8 host name for presentation to the user.
|
||||
|
||||
### hostname
|
||||
|
||||
Hostname is the fallback value for all 3 fields above, if only `hostname` is
|
||||
specified, it will set all 3 fields to this value.
|
||||
|
||||
## KV
|
||||
|
||||
The KV resource sets a key and value pair in the global world database. This is
|
||||
quite useful for setting a flag after a number of resources have run. It will
|
||||
ignore database updates to the value that are greater in compare order than the
|
||||
requested key if the `SkipLessThan` parameter is set to true. If we receive a
|
||||
refresh, then the stored value will be reset to the requested value even if the
|
||||
stored value is greater.
|
||||
|
||||
### Key
|
||||
|
||||
The string key used to store the key.
|
||||
|
||||
### Value
|
||||
|
||||
The string value to set. This can also be set via Send/Recv.
|
||||
|
||||
### SkipLessThan
|
||||
|
||||
If this parameter is set to `true`, then it will ignore updating the value as
|
||||
long as the database versions are greater than the requested value. The compare
|
||||
operation used is based on the `SkipCmpStyle` parameter.
|
||||
|
||||
### SkipCmpStyle
|
||||
|
||||
By default this converts the string values to integers and compares them as you
|
||||
would expect.
|
||||
|
||||
## Msg
|
||||
|
||||
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
|
||||
`mgmt` and also as a placeholder in the resource graph.
|
||||
|
||||
## Nspawn
|
||||
|
||||
The nspawn resource is used to manage systemd-machined style containers.
|
||||
|
||||
## Password
|
||||
|
||||
The password resource can generate a random string to be used as a password. It
|
||||
will re-generate the password if it receives a refresh notification.
|
||||
|
||||
## Pkg
|
||||
|
||||
The pkg resource is used to manage system packages. This resource works on many
|
||||
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 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 by improving it!
|
||||
|
||||
## User
|
||||
|
||||
The user resource manages the system users from `/etc/passwd`.
|
||||
|
||||
## Virt
|
||||
|
||||
The virt resource can manage virtual machines via libvirt.
|
||||
225
docs/style-guide.md
Normal file
225
docs/style-guide.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Style guide
|
||||
|
||||
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!
|
||||
|
||||
## 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
|
||||
|
||||
* All of our golang code is formatted with `gofmt`.
|
||||
|
||||
### 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
|
||||
sentences (which end with a period, use proper punctuation, and capitalize the
|
||||
first word when it is not a lower cased identifier), or are short one-line
|
||||
comments in the source which are not full sentences and don't end with a period.
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
private.
|
||||
|
||||
```golang
|
||||
// square multiplies the input integer by itself and returns this product.
|
||||
func square(x int) int {
|
||||
return x * x // we don't care about overflow errors
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
paragraphs. Exceptions are always allowed based on the will of the maintainer.
|
||||
|
||||
It is usually better to exceed 80 characters than to break code unnecessarily.
|
||||
If your code often exceeds 80 characters, it might be an indication that it
|
||||
needs refactoring.
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
of these `obj` for ease of code copying throughout the project, and for faster
|
||||
identification when reviewing code. Some anecdotal studies have shown that it
|
||||
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
|
||||
|
||||
```golang
|
||||
// Bar does a thing, and returns the number of baz results found in our
|
||||
database.
|
||||
func (obj *Foo) Bar(baz string) int {
|
||||
if len(obj.s) > 0 {
|
||||
return strings.Count(obj.s, baz)
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
This is also the order which is recommended when creating interface types. When
|
||||
implementing an interface, arrange your methods in the same order that they are
|
||||
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,
|
||||
please let us know!
|
||||
126
engine/autoedge.go
Normal file
126
engine/autoedge.go
Normal file
@@ -0,0 +1,126 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
IFF(ResUID) bool
|
||||
|
||||
IsReversed() bool // true means this resource happens before the generator
|
||||
}
|
||||
|
||||
// The BaseUID struct is used to provide a unique resource identifier.
|
||||
type BaseUID struct {
|
||||
Name string // name and kind are the values of where this is coming from
|
||||
Kind string
|
||||
|
||||
Reversed *bool // piggyback edge information here
|
||||
}
|
||||
|
||||
// GetName returns the name of the resource UID.
|
||||
func (obj *BaseUID) GetName() string {
|
||||
return obj.Name
|
||||
}
|
||||
|
||||
// GetKind returns the kind of the resource UID.
|
||||
func (obj *BaseUID) GetKind() string {
|
||||
return obj.Kind
|
||||
}
|
||||
|
||||
// String returns the canonical string representation for a resource UID.
|
||||
func (obj *BaseUID) String() string {
|
||||
return fmt.Sprintf("%s[%s]", obj.GetKind(), obj.GetName())
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *BaseUID) IFF(uid ResUID) bool {
|
||||
res, ok := uid.(*BaseUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return obj.Name == res.Name
|
||||
}
|
||||
|
||||
// IsReversed is part of the ResUID interface, and true means this resource
|
||||
// happens before the generator.
|
||||
func (obj *BaseUID) IsReversed() bool {
|
||||
if obj.Reversed == nil {
|
||||
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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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-2023+ 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-2023+ 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
|
||||
}
|
||||
21
engine/doc.go
Normal file
21
engine/doc.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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 represents the implementation of the resource engine that runs
|
||||
// the graph of resources in real-time. This package has the common imports that
|
||||
// most consumers use directly.
|
||||
package engine
|
||||
60
engine/edge.go
Normal file
60
engine/edge.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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"
|
||||
)
|
||||
|
||||
// Edge is a struct that represents a graph's edge.
|
||||
type Edge struct {
|
||||
Name string
|
||||
Notify bool // should we send a refresh notification along this edge?
|
||||
|
||||
refresh bool // is there a notify pending for the dest vertex ?
|
||||
}
|
||||
|
||||
// String is a required method of the Edge interface that we must fulfill.
|
||||
func (obj *Edge) String() string {
|
||||
return obj.Name
|
||||
}
|
||||
|
||||
// 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 fmt.Errorf("edge names differ")
|
||||
}
|
||||
if obj.Notify != edge.Notify {
|
||||
return fmt.Errorf("notify values differ")
|
||||
}
|
||||
// FIXME: should we compare this as well?
|
||||
//if obj.refresh != edge.refresh {
|
||||
// return fmt.Errorf("refresh values differ")
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh returns the pending refresh status of this edge.
|
||||
func (obj *Edge) Refresh() bool {
|
||||
return obj.refresh
|
||||
}
|
||||
|
||||
// SetRefresh sets the pending refresh status of this edge.
|
||||
func (obj *Edge) SetRefresh(b bool) {
|
||||
obj.refresh = b
|
||||
}
|
||||
29
engine/error.go
Normal file
29
engine/error.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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")
|
||||
)
|
||||
61
engine/fs.go
Normal file
61
engine/fs.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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 (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// from the ioutil package:
|
||||
// NopCloser(r io.Reader) io.ReadCloser // not implemented here
|
||||
// ReadAll(r io.Reader) ([]byte, error)
|
||||
// ReadDir(dirname string) ([]os.FileInfo, error)
|
||||
// ReadFile(filename string) ([]byte, error)
|
||||
// TempDir(dir, prefix string) (name string, err error)
|
||||
// TempFile(dir, prefix string) (f *os.File, err error) // slightly different here
|
||||
// WriteFile(filename string, data []byte, perm os.FileMode) error
|
||||
|
||||
// Fs is an interface that represents this file system API that we support.
|
||||
// TODO: this should be in the gapi package or elsewhere.
|
||||
type Fs interface {
|
||||
//fmt.Stringer // TODO: add this method?
|
||||
afero.Fs // TODO: why doesn't this interface exist in the os pkg?
|
||||
URI() string // returns the URI for this file system
|
||||
|
||||
//DirExists(path string) (bool, error)
|
||||
//Exists(path string) (bool, error)
|
||||
//FileContainsAnyBytes(filename string, subslices [][]byte) (bool, error)
|
||||
//FileContainsBytes(filename string, subslice []byte) (bool, error)
|
||||
//FullBaseFsPath(basePathFs *BasePathFs, relativePath string) string
|
||||
//GetTempDir(subPath string) string
|
||||
//IsDir(path string) (bool, error)
|
||||
//IsEmpty(path string) (bool, error)
|
||||
//NeuterAccents(s string) string
|
||||
//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)
|
||||
TempDir(dir, prefix string) (name string, err error)
|
||||
TempFile(dir, prefix string) (f afero.File, err error) // slightly different from upstream
|
||||
//UnicodeSanitize(s string) string
|
||||
//Walk(root string, walkFn filepath.WalkFunc) error
|
||||
WriteFile(filename string, data []byte, perm os.FileMode) error
|
||||
//WriteReader(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-2023+ 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-2023+ 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)
|
||||
}
|
||||
156
engine/graph/autoedge/autoedge.go
Normal file
156
engine/graph/autoedge/autoedge.go
Normal file
@@ -0,0 +1,156 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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 autoedge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
// 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...")
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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(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
|
||||
|
||||
// loop through each uid, and see if it matches any vertex
|
||||
for _, uid := range uids {
|
||||
var found = false
|
||||
// uid is a ResUID object
|
||||
for _, v := range graph.Vertices() { // search
|
||||
r, ok := v.(engine.EdgeableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
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, r.UIDs()) {
|
||||
// add edge from: r -> res
|
||||
if uid.IsReversed() {
|
||||
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("%s -> %s (autoedge)", res, r)
|
||||
logf("autoedge: adding: %s", txt)
|
||||
edge := &engine.Edge{Name: txt}
|
||||
graph.AddEdge(res, r, edge)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, found)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 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 false
|
||||
}
|
||||
140
engine/graph/autogroup.go
Normal file
140
engine/graph/autogroup.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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-2023+ 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
|
||||
}
|
||||
938
engine/graph/autogroup/autogroup_test.go
Normal file
938
engine/graph/autogroup/autogroup_test.go
Normal file
@@ -0,0 +1,938 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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/>.
|
||||
|
||||
//go:build !root
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"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 := &engine.Edge{Name: s}
|
||||
return obj
|
||||
}
|
||||
|
||||
type testGrouper struct {
|
||||
// TODO: this algorithm may not be correct in all cases. replace if needed!
|
||||
NonReachabilityGrouper // "inherit" what we want, and reimplement the rest
|
||||
}
|
||||
|
||||
func (obj *testGrouper) Name() string {
|
||||
return "testGrouper"
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
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 (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 &engine.Edge{Name: strings.Join(names, ",")}
|
||||
}
|
||||
|
||||
// helper function
|
||||
func runGraphCmp(t *testing.T, g1, g2 *pgraph.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))
|
||||
t.Logf("expected (g2): %v%v", g2, fullPrint(g2))
|
||||
t.Logf("Cmp error:")
|
||||
t.Errorf("%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func GraphCmp(g1, g2 *pgraph.Graph) error {
|
||||
if n1, n2 := g1.NumVertices(), g2.NumVertices(); n1 != n2 {
|
||||
return fmt.Errorf("graph g1 has %d vertices, while g2 has %d", n1, n2)
|
||||
}
|
||||
if e1, e2 := g1.NumEdges(), g2.NumEdges(); e1 != e2 {
|
||||
return fmt.Errorf("graph g1 has %d edges, while g2 has %d", e1, e2)
|
||||
}
|
||||
|
||||
var m = make(map[pgraph.Vertex]pgraph.Vertex) // g1 to g2 vertex correspondence
|
||||
Loop:
|
||||
// check vertices
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
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 ?
|
||||
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)
|
||||
|
||||
// does l1 match l2 ?
|
||||
if ListStrCmp(l1, l2) { // cmp!
|
||||
m[v1] = v2
|
||||
continue Loop
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("graph g1, has no match in g2 for: %v", r1.Name())
|
||||
}
|
||||
// vertices (and groups) match :)
|
||||
|
||||
// check edges
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
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 {
|
||||
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.(*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!)
|
||||
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(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", rr1.Name(), rr2.Name())
|
||||
}
|
||||
|
||||
// check: (2) ee1 == ee2
|
||||
if ee1.Name != ee2.Name {
|
||||
return fmt.Errorf("graph g1 edge(%v) doesn't match g2 edge(%v)", ee1.Name, ee2.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check meta parameters
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
for v2 := range g2.Adjacency() { // does it match in g2 ?
|
||||
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", r1.Name(), r2.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil // success!
|
||||
}
|
||||
|
||||
// ListStrCmp compares two lists of strings
|
||||
func ListStrCmp(a, b []string) bool {
|
||||
//fmt.Printf("CMP: %v with %v\n", a, b) // debugging
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func fullPrint(g *pgraph.Graph) (str string) {
|
||||
str += "\n"
|
||||
for v := range g.Adjacency() {
|
||||
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", r.Name())
|
||||
}
|
||||
// TODO: add explicit grouping data?
|
||||
}
|
||||
for v1 := range g.Adjacency() {
|
||||
for v2, e := range g.Adjacency()[v1] {
|
||||
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
|
||||
}
|
||||
|
||||
func TestDurationAssumptions(t *testing.T) {
|
||||
var d time.Duration
|
||||
if (d == 0) != true {
|
||||
t.Errorf("empty time.Duration is no longer equal to zero")
|
||||
}
|
||||
if (d > 0) != false {
|
||||
t.Errorf("empty time.Duration is now greater than zero")
|
||||
}
|
||||
}
|
||||
|
||||
// all of the following test cases are laid out with the following semantics:
|
||||
// * vertices which start with the same single letter are considered "like"
|
||||
// * "like" elements should be merged
|
||||
// * vertices can have any integer after their single letter "family" type
|
||||
// * grouped vertices should have a name with a comma separated list of names
|
||||
// * edges follow the same conventions about grouping
|
||||
|
||||
// empty graph
|
||||
func TestPgraphGrouping1(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// single vertex
|
||||
func TestPgraphGrouping2(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{ // grouping to limit variable scope
|
||||
a1 := NewNoopResTest("a1")
|
||||
g1.AddVertex(a1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
g2.AddVertex(a1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// two vertices
|
||||
func TestPgraphGrouping3(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g1.AddVertex(a1, b1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g2.AddVertex(a1, b1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// two vertices merge
|
||||
func TestPgraphGrouping4(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
g1.AddVertex(a1, a2)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
g2.AddVertex(a)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// three vertices merge
|
||||
func TestPgraphGrouping5(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
a3 := NewNoopResTest("a3")
|
||||
g1.AddVertex(a1, a2, a3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2,a3")
|
||||
g2.AddVertex(a)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// three vertices, two merge
|
||||
func TestPgraphGrouping6(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g1.AddVertex(a1, a2, b1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g2.AddVertex(a, b1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// four vertices, three merge
|
||||
func TestPgraphGrouping7(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
a3 := NewNoopResTest("a3")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g1.AddVertex(a1, a2, a3, b1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2,a3")
|
||||
b1 := NewNoopResTest("b1")
|
||||
g2.AddVertex(a, b1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// four vertices, two&two merge
|
||||
func TestPgraphGrouping8(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
g1.AddVertex(a1, a2, b1, b2)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b := NewNoopResTest("b1,b2")
|
||||
g2.AddVertex(a, b)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// five vertices, two&three merge
|
||||
func TestPgraphGrouping9(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
b3 := NewNoopResTest("b3")
|
||||
g1.AddVertex(a1, a2, b1, b2, b3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b := NewNoopResTest("b1,b2,b3")
|
||||
g2.AddVertex(a, b)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// three unique vertices
|
||||
func TestPgraphGrouping10(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
c1 := NewNoopResTest("c1")
|
||||
g1.AddVertex(a1, b1, c1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
c1 := NewNoopResTest("c1")
|
||||
g2.AddVertex(a1, b1, c1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
// three unique vertices, two merge
|
||||
func TestPgraphGrouping11(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
g1.AddVertex(a1, b1, b2, c1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b := NewNoopResTest("b1,b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
g2.AddVertex(a1, b, c1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// simple merge 1
|
||||
// a1 a2 a1,a2
|
||||
// \ / >>> | (arrows point downwards)
|
||||
// b b
|
||||
*/
|
||||
func TestPgraphGrouping12(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(a2, b1, e2)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e := NE("e1,e2")
|
||||
g2.AddEdge(a, b1, e)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// simple merge 2
|
||||
// b b
|
||||
// / \ >>> | (arrows point downwards)
|
||||
// a1 a2 a1,a2
|
||||
*/
|
||||
func TestPgraphGrouping13(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
g1.AddEdge(b1, a1, e1)
|
||||
g1.AddEdge(b1, a2, e2)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e := NE("e1,e2")
|
||||
g2.AddEdge(b1, a, e)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// triple merge
|
||||
// a1 a2 a3 a1,a2,a3
|
||||
// \ | / >>> | (arrows point downwards)
|
||||
// b b
|
||||
*/
|
||||
func TestPgraphGrouping14(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
a3 := NewNoopResTest("a3")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
e3 := NE("e3")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(a2, b1, e2)
|
||||
g1.AddEdge(a3, b1, e3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2,a3")
|
||||
b1 := NewNoopResTest("b1")
|
||||
e := NE("e1,e2,e3")
|
||||
g2.AddEdge(a, b1, e)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// chain merge
|
||||
// a1 a1
|
||||
// / \ |
|
||||
// b1 b2 >>> b1,b2 (arrows point downwards)
|
||||
// \ / |
|
||||
// c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping15(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
e3 := NE("e3")
|
||||
e4 := NE("e4")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(a1, b2, e2)
|
||||
g1.AddEdge(b1, c1, e3)
|
||||
g1.AddEdge(b2, c1, e4)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b := NewNoopResTest("b1,b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1,e2")
|
||||
e2 := NE("e3,e4")
|
||||
g2.AddEdge(a1, b, e1)
|
||||
g2.AddEdge(b, c1, e2)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// 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
|
||||
// | / | | \
|
||||
// b1 / >>> b1 OR b1 / (arrows point downwards)
|
||||
// | / | | /
|
||||
// c1 c1 c1
|
||||
*/
|
||||
func TestPgraphGrouping16(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
e3 := NE("e3")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(b1, c1, e2)
|
||||
g1.AddEdge(a2, c1, e3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1,e3")
|
||||
e2 := NE("e2,e3") // e3 gets "merged through" to BOTH edges!
|
||||
g2.AddEdge(a, b1, e1)
|
||||
g2.AddEdge(b1, c1, e2)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// 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
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
e3 := NE("e3")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(b1, c1, e2)
|
||||
g1.AddEdge(b2, c1, e3)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b := NewNoopResTest("b1,b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2,e3")
|
||||
g2.AddEdge(a1, b, e1)
|
||||
g2.AddEdge(b, c1, e2)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// 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
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
b1 := NewNoopResTest("b1")
|
||||
b2 := NewNoopResTest("b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
e3 := NE("e3")
|
||||
e4 := NE("e4")
|
||||
g1.AddEdge(a1, b1, e1)
|
||||
g1.AddEdge(b1, c1, e2)
|
||||
g1.AddEdge(a2, c1, e3)
|
||||
g1.AddEdge(b2, c1, e4)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result
|
||||
{
|
||||
a := NewNoopResTest("a1,a2")
|
||||
b := NewNoopResTest("b1,b2")
|
||||
c1 := NewNoopResTest("c1")
|
||||
e1 := NE("e1,e3")
|
||||
e2 := NE("e2,e3,e4") // e3 gets "merged through" to BOTH edges!
|
||||
g2.AddEdge(a, b, e1)
|
||||
g2.AddEdge(b, c1, e2)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// connected merge 0, (no change!)
|
||||
// a1 a1
|
||||
// \ >>> \ (arrows point downwards)
|
||||
// a2 a2
|
||||
*/
|
||||
func TestPgraphGroupingConnected0(t *testing.T) {
|
||||
g1, _ := pgraph.NewGraph("g1") // original graph
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
e1 := NE("e1")
|
||||
g1.AddEdge(a1, a2, e1)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result ?
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
a2 := NewNoopResTest("a2")
|
||||
e1 := NE("e1")
|
||||
g2.AddEdge(a1, a2, e1)
|
||||
}
|
||||
runGraphCmp(t, g1, g2)
|
||||
}
|
||||
|
||||
/*
|
||||
// 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
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b := NewNoopResTest("b")
|
||||
a2 := NewNoopResTest("a2")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
g1.AddEdge(a1, b, e1)
|
||||
g1.AddEdge(b, a2, e2)
|
||||
}
|
||||
g2, _ := pgraph.NewGraph("g2") // expected result ?
|
||||
{
|
||||
a1 := NewNoopResTest("a1")
|
||||
b := NewNoopResTest("b")
|
||||
a2 := NewNoopResTest("a2")
|
||||
e1 := NE("e1")
|
||||
e2 := NE("e2")
|
||||
g2.AddEdge(a1, b, e1)
|
||||
g2.AddEdge(b, a2, e2)
|
||||
}
|
||||
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-2023+ 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-2023+ 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-2023+ 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
|
||||
}
|
||||
435
engine/graph/engine.go
Normal file
435
engine/graph/engine.go
Normal file
@@ -0,0 +1,435 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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 contains the actual implementation of the resource graph engine
|
||||
// that runs the graph of resources in real-time. This package has the algorithm
|
||||
// that runs all the graph transitions.
|
||||
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
|
||||
Version 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,
|
||||
Version: obj.Version,
|
||||
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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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-2023+ 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")
|
||||
}
|
||||
84
engine/graph/semaphore.go
Normal file
84
engine/graph/semaphore.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
"github.com/purpleidea/mgmt/util/semaphore"
|
||||
)
|
||||
|
||||
// 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 *Engine) semaLock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // very important to avoid deadlock in the dag!
|
||||
|
||||
for _, id := range semas {
|
||||
obj.slock.Lock() // semaphore creation lock
|
||||
sema, ok := obj.semas[id] // lookup
|
||||
if !ok {
|
||||
size := SemaSize(id) // defaults to 1
|
||||
obj.semas[id] = semaphore.NewSemaphore(size)
|
||||
sema = obj.semas[id]
|
||||
}
|
||||
obj.slock.Unlock()
|
||||
|
||||
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 *Engine) semaUnlock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // unlock in the same order to remove partial locks
|
||||
|
||||
for _, id := range semas {
|
||||
sema, ok := obj.semas[id] // lookup
|
||||
if !ok {
|
||||
// programming error!
|
||||
panic(fmt.Sprintf("graph: sema: %s does not exist", id))
|
||||
}
|
||||
|
||||
err := sema.V(1) // unlock!
|
||||
reterr = errwrap.Append(reterr, err) // list of errors
|
||||
}
|
||||
return reterr
|
||||
}
|
||||
|
||||
// SemaSize returns the size integer associated with the semaphore id. It
|
||||
// defaults to 1 if not found.
|
||||
func SemaSize(id string) int {
|
||||
size := 1 // default semaphore size
|
||||
// valid id's include "some_id", "hello:42" and ":13"
|
||||
if index := strings.LastIndex(id, SemaSep); index > -1 && (len(id)-index+len(SemaSep)) >= 1 {
|
||||
// NOTE: we only allow size > 0 here!
|
||||
if i, err := strconv.Atoi(id[index+len(SemaSep):]); err == nil && i > 0 {
|
||||
size = i
|
||||
}
|
||||
}
|
||||
return size
|
||||
}
|
||||
37
engine/graph/semaphore_test.go
Normal file
37
engine/graph/semaphore_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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
|
||||
}
|
||||
410
engine/graph/state.go
Normal file
410
engine/graph/state.go
Normal file
@@ -0,0 +1,410 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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
|
||||
Version 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,
|
||||
Version: obj.Version,
|
||||
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, err
|
||||
}
|
||||
|
||||
// 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-2023+ 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-2023+ 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-2023+ 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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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)
|
||||
}
|
||||
317
engine/resources.go
Normal file
317
engine/resources.go
Normal file
@@ -0,0 +1,317 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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
|
||||
|
||||
// Version is the version of the program.
|
||||
Version 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
|
||||
}
|
||||
320
engine/resources/augeas.go
Normal file
320
engine/resources/augeas.go
Normal file
@@ -0,0 +1,320 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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/>.
|
||||
|
||||
//go:build !noaugeas
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
|
||||
"honnef.co/go/augeas"
|
||||
)
|
||||
|
||||
const (
|
||||
// NS is a namespace for augeas operations
|
||||
NS = "Xmgmt"
|
||||
)
|
||||
|
||||
func init() {
|
||||
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 {
|
||||
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"`
|
||||
|
||||
// Lens is the lens used by this resource. If specified, mgmt
|
||||
// will lower the augeas overhead by only loading that lens.
|
||||
Lens string `yaml:"lens"`
|
||||
|
||||
// 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"`
|
||||
|
||||
recWatcher *recwatch.RecWatcher // used to watch the changed files
|
||||
}
|
||||
|
||||
// AugeasSet represents a key/value pair of settings to be applied.
|
||||
type AugeasSet struct {
|
||||
Path string `yaml:"path"` // The relative path to the value to be changed.
|
||||
Value string `yaml:"value"` // The value to be set on the given Path.
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *AugeasRes) Validate() error {
|
||||
if !strings.HasPrefix(obj.File, "/") {
|
||||
return fmt.Errorf("the File param should start with a slash")
|
||||
}
|
||||
if obj.Lens != "" && !strings.HasSuffix(obj.Lens, ".lns") {
|
||||
return fmt.Errorf("the Lens param should have a .lns suffix")
|
||||
}
|
||||
if (obj.Lens == "") != (obj.File == "") {
|
||||
return fmt.Errorf("the File and Lens params must be specified together")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the resource.
|
||||
func (obj *AugeasRes) 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 *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
|
||||
obj.recWatcher, err = recwatch.NewRecWatcher(obj.File, 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 {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", obj.File) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
case event, ok := <-obj.recWatcher.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 { // 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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkApplySet runs CheckApply for one element of the AugeasRes.Set
|
||||
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
|
||||
// the path does not exist.
|
||||
if getValue, _ := ag.Get(fullpath); set.Value == getValue {
|
||||
// The value is what we expect, return directly
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if !apply {
|
||||
// If noop, we can return here directly. We return with
|
||||
// nil even if err is not nil because it does not mean
|
||||
// there is an error.
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if err := ag.Set(fullpath, set.Value); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while setting value")
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// CheckApply method for Augeas resource.
|
||||
func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
|
||||
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 != "" {
|
||||
// if the lens is specified, we can speed up augeas by not
|
||||
// loading everything. Without this option, augeas will try to
|
||||
// read all the files it knows in the complete filesystem.
|
||||
// e.g. to change /etc/ssh/sshd_config, it would load /etc/hosts, /etc/ntpd.conf, etc...
|
||||
opts = augeas.NoModlAutoload
|
||||
}
|
||||
|
||||
// Initiate augeas
|
||||
ag, err := augeas.New("/", "", opts)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while initializing")
|
||||
}
|
||||
defer ag.Close()
|
||||
|
||||
if obj.Lens != "" {
|
||||
// If the lens is set, load the lens for the file we want to edit.
|
||||
// We pick Xmgmt, as this name will not collide with any other lens name.
|
||||
// We do not pick Mgmt as in the future there might be an Mgmt lens.
|
||||
// https://github.com/hercules-team/augeas/wiki/Loading-specific-files
|
||||
if err = ag.Set(fmt.Sprintf("/augeas/load/%s/lens", NS), obj.Lens); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while initializing lens")
|
||||
}
|
||||
if err = ag.Set(fmt.Sprintf("/augeas/load/%s/incl", NS), obj.File); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while initializing incl")
|
||||
}
|
||||
if err = ag.Load(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while loading")
|
||||
}
|
||||
}
|
||||
|
||||
checkOK := true
|
||||
for _, set := range obj.Sets {
|
||||
if setCheckOK, err := obj.checkApplySet(apply, &ag, set); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error during CheckApply of one Set")
|
||||
} else if !setCheckOK {
|
||||
checkOK = false
|
||||
}
|
||||
}
|
||||
|
||||
// If the state is correct or we can't apply, return early.
|
||||
if checkOK || !apply {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
obj.init.Logf("changes needed, saving")
|
||||
if err = ag.Save(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "augeas: error while saving augeas values")
|
||||
}
|
||||
|
||||
// FIXME: Workaround for https://github.com/dominikh/go-augeas/issues/13
|
||||
// To be fixed upstream.
|
||||
if obj.File != "" {
|
||||
if _, err := os.Stat(obj.File); os.IsNotExist(err) {
|
||||
return false, errwrap.Wrapf(err, "augeas: error: file does not exist")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// UIDs includes all params to make a unique identification of this object.
|
||||
func (obj *AugeasRes) UIDs() []engine.ResUID {
|
||||
x := &AugeasUID{
|
||||
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 *AugeasRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes AugeasRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*AugeasRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to AugeasRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = AugeasRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
1416
engine/resources/aws_ec2.go
Normal file
1416
engine/resources/aws_ec2.go
Normal file
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-2023+ 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-2023+ 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-2023+ 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-2023+ 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/v22/dbus"
|
||||
"github.com/coreos/go-systemd/v22/unit"
|
||||
systemdUtil "github.com/coreos/go-systemd/v22/util"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
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
19
engine/resources/doc.go
Normal file
19
engine/resources/doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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 contains the implementations of all the core resources.
|
||||
package resources
|
||||
493
engine/resources/docker_container.go
Normal file
493
engine/resources/docker_container.go
Normal file
@@ -0,0 +1,493 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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-2023+ 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/>.
|
||||
|
||||
//go: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-2023+ 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/>.
|
||||
|
||||
//go: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")
|
||||
}
|
||||
}
|
||||
303
engine/resources/group.go
Normal file
303
engine/resources/group.go
Normal file
@@ -0,0 +1,303 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2023+ 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"
|
||||
"io/ioutil"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("group", func() engine.Res { return &GroupRes{} })
|
||||
}
|
||||
|
||||
const groupFile = "/etc/group"
|
||||
|
||||
// GroupRes is a user group resource.
|
||||
type GroupRes struct {
|
||||
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() 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 nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *GroupRes) Watch() error {
|
||||
var err error
|
||||
obj.recWatcher, err = recwatch.NewRecWatcher(groupFile, 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 {
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", groupFile) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
case event, ok := <-obj.recWatcher.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 { // 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
|
||||
|
||||
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 Group resource.
|
||||
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.Name())
|
||||
if err != nil {
|
||||
if _, ok := err.(user.UnknownGroupError); !ok {
|
||||
return false, errwrap.Wrapf(err, "error looking up group")
|
||||
}
|
||||
exists = false
|
||||
}
|
||||
// if the group doesn't exist and should be absent, we are done
|
||||
if obj.State == "absent" && !exists {
|
||||
return true, nil
|
||||
}
|
||||
// if the group exists and no GID is specified, we are done
|
||||
if obj.State == "exists" && exists && obj.GID == nil {
|
||||
return true, nil
|
||||
}
|
||||
if exists && obj.GID != nil {
|
||||
// check if GID is taken
|
||||
lookupGID, err := user.LookupGroupId(strconv.Itoa(int(*obj.GID)))
|
||||
if err != nil {
|
||||
if _, ok := err.(user.UnknownGroupIdError); !ok {
|
||||
return false, errwrap.Wrapf(err, "error looking up GID")
|
||||
}
|
||||
}
|
||||
if lookupGID != nil && lookupGID.Name != obj.Name() {
|
||||
return false, fmt.Errorf("the requested GID belongs to another group")
|
||||
}
|
||||
// get the existing group's GID
|
||||
existingGID, err := strconv.ParseUint(group.Gid, 10, 32)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error casting existing GID")
|
||||
}
|
||||
// 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) {
|
||||
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) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
var cmdName string
|
||||
args := []string{obj.Name()}
|
||||
|
||||
if obj.State == "exists" {
|
||||
if exists {
|
||||
obj.init.Logf("Modifying group: %s", obj.Name())
|
||||
cmdName = "groupmod"
|
||||
} else {
|
||||
obj.init.Logf("Adding group: %s", obj.Name())
|
||||
cmdName = "groupadd"
|
||||
}
|
||||
if obj.GID != nil {
|
||||
args = append(args, "-g", fmt.Sprintf("%d", *obj.GID))
|
||||
}
|
||||
}
|
||||
if obj.State == "absent" && exists {
|
||||
obj.init.Logf("Deleting group: %s", obj.Name())
|
||||
cmdName = "groupdel"
|
||||
}
|
||||
|
||||
cmd := exec.Command(cmdName, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
// open a pipe to get error messages from os/exec
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "failed to initialize stderr pipe")
|
||||
}
|
||||
|
||||
// start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "cmd failed to start")
|
||||
}
|
||||
// capture any error messages
|
||||
slurp, err := ioutil.ReadAll(stderr)
|
||||
if err != nil {
|
||||
return false, errwrap.Wrapf(err, "error slurping error message")
|
||||
}
|
||||
// wait until cmd exits and return error message if any
|
||||
if err := cmd.Wait(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "%s", slurp)
|
||||
}
|
||||
|
||||
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 {
|
||||
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 engine.ResUID) bool {
|
||||
res, ok := uid.(*GroupUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if obj.gid != nil && res.gid != nil {
|
||||
if *obj.gid != *res.gid {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if obj.name != "" && res.name != "" {
|
||||
if obj.name != res.name {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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() []engine.ResUID {
|
||||
x := &GroupUID{
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
gid: obj.GID,
|
||||
}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*GroupRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to GroupRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = GroupRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
1227
engine/resources/hetzner_vm.go
Normal file
1227
engine/resources/hetzner_vm.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