Compare commits
586 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -12,8 +12,14 @@ end_of_line = lf
|
|||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_style = tab
|
||||||
|
|
||||||
[*.go]
|
[*.go]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
[Makefile]
|
[Makefile]
|
||||||
indent_style = tab
|
indent_style = tab
|
||||||
|
|
||||||
|
[*.mcl]
|
||||||
|
indent_style = tab
|
||||||
|
|||||||
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!
|
||||||
96
.github/settings.yml
vendored
Normal file
96
.github/settings.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# 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: 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
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -2,11 +2,18 @@
|
|||||||
.omv/
|
.omv/
|
||||||
.ssh/
|
.ssh/
|
||||||
.vagrant/
|
.vagrant/
|
||||||
mgmt-documentation.pdf
|
.envrc
|
||||||
old/
|
old/
|
||||||
tmp/
|
tmp/
|
||||||
|
*WIP
|
||||||
*_stringer.go
|
*_stringer.go
|
||||||
|
bindata/*.go
|
||||||
mgmt
|
mgmt
|
||||||
mgmt.static
|
mgmt.static
|
||||||
|
# crossbuild artifacts
|
||||||
|
build/mgmt-*
|
||||||
mgmt.iml
|
mgmt.iml
|
||||||
rpmbuild/
|
rpmbuild/
|
||||||
|
releases/
|
||||||
|
# vim swap files
|
||||||
|
.*.sw[op]
|
||||||
|
|||||||
18
.gitmodules
vendored
18
.gitmodules
vendored
@@ -13,3 +13,21 @@
|
|||||||
[submodule "vendor/github.com/purpleidea/go-systemd"]
|
[submodule "vendor/github.com/purpleidea/go-systemd"]
|
||||||
path = vendor/github.com/purpleidea/go-systemd
|
path = vendor/github.com/purpleidea/go-systemd
|
||||||
url = https://github.com/purpleidea/go-systemd
|
url = https://github.com/purpleidea/go-systemd
|
||||||
|
[submodule "vendor/honnef.co/go/augeas"]
|
||||||
|
path = vendor/honnef.co/go/augeas
|
||||||
|
url = https://github.com/dominikh/go-augeas/
|
||||||
|
[submodule "vendor/github.com/grpc-ecosystem/go-grpc-prometheus"]
|
||||||
|
path = vendor/github.com/grpc-ecosystem/go-grpc-prometheus
|
||||||
|
url = https://github.com/grpc-ecosystem/go-grpc-prometheus
|
||||||
|
[submodule "vendor/github.com/ugorji/go"]
|
||||||
|
path = vendor/github.com/ugorji/go
|
||||||
|
url = https://github.com/ugorji/go
|
||||||
|
[submodule "vendor/github.com/purpleidea/docker"]
|
||||||
|
path = vendor/github.com/docker/docker
|
||||||
|
url = https://github.com/purpleidea/docker
|
||||||
|
[submodule "vendor/github.com/purpleidea/distribution"]
|
||||||
|
path = vendor/github.com/docker/distribution
|
||||||
|
url = https://github.com/purpleidea/distribution
|
||||||
|
[submodule "vendor/github.com/purpleidea/go-connections"]
|
||||||
|
path = vendor/github.com/docker/go-connections
|
||||||
|
url = https://github.com/docker/go-connections
|
||||||
|
|||||||
45
.travis.yml
45
.travis.yml
@@ -1,22 +1,48 @@
|
|||||||
language: go
|
language: go
|
||||||
|
os:
|
||||||
|
- linux
|
||||||
go:
|
go:
|
||||||
- 1.6
|
- 1.10.x
|
||||||
- 1.7
|
- 1.11.x
|
||||||
- tip
|
- tip
|
||||||
|
go_import_path: github.com/purpleidea/mgmt
|
||||||
sudo: true
|
sudo: true
|
||||||
dist: trusty
|
dist: xenial
|
||||||
before_install: 'git fetch --unshallow'
|
# 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'
|
install: 'make deps'
|
||||||
script: 'make test'
|
script: 'make test'
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: false
|
||||||
allow_failures:
|
allow_failures:
|
||||||
|
- go: 1.11.x
|
||||||
- go: tip
|
- go: tip
|
||||||
- go: 1.7
|
- os: osx
|
||||||
|
# include only one build for osx for a quicker build as the nr. of these runners are sparse
|
||||||
|
include:
|
||||||
|
- os: osx
|
||||||
|
go: 1.10.x
|
||||||
|
|
||||||
|
# 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:
|
notifications:
|
||||||
irc:
|
irc:
|
||||||
channels:
|
channels:
|
||||||
- "irc.freenode.net#mgmtconfig"
|
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
|
||||||
template:
|
template:
|
||||||
- "%{repository} (%{commit}: %{author}): %{message}"
|
- "%{repository} (%{commit}: %{author}): %{message}"
|
||||||
- "More info : %{build_url}"
|
- "More info : %{build_url}"
|
||||||
@@ -25,4 +51,7 @@ notifications:
|
|||||||
use_notice: false
|
use_notice: false
|
||||||
skip_join: false
|
skip_join: false
|
||||||
email:
|
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
|
||||||
|
|||||||
6
AUTHORS
6
AUTHORS
@@ -1,8 +1,12 @@
|
|||||||
This is a list of authors/contributors to the mgmt project.
|
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!
|
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
|
For a more exhaustive list please run: git log --format='%aN' | sort -u
|
||||||
This list is sorted alphabetically by first name.
|
This list is sorted alphabetically by first name.
|
||||||
|
|
||||||
|
Felix Frank
|
||||||
James Shubin
|
James Shubin
|
||||||
|
Johan Bloemberg
|
||||||
|
Jonathan Gold
|
||||||
|
Julien Pivotto
|
||||||
Paul Morgan
|
Paul Morgan
|
||||||
|
|||||||
141
COPYING
141
COPYING
@@ -1,5 +1,5 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
@@ -7,15 +7,17 @@
|
|||||||
|
|
||||||
Preamble
|
Preamble
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
The GNU General Public License is a free, copyleft license for
|
||||||
software and other kinds of works, specifically designed to ensure
|
software and other kinds of works.
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
The licenses for most software and other practical works are designed
|
||||||
to take away your freedom to share and change the works. By contrast,
|
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
|
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
|
When we speak of free software, we are referring to freedom, not
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
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
|
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.
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
To protect your rights, we need to prevent others from denying you
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
you this License which gives you legal permission to copy, distribute
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
and/or modify the software.
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
For example, if you distribute copies of such a program, whether
|
||||||
improvements made in alternate versions of the program, if they
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
receive widespread use, become available for other developers to
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
incorporate. Many developers of free software are heartened and
|
or can get the source code. And you must show them these terms so they
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
know their rights.
|
||||||
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.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
ensure that, in such cases, the modified source code becomes available
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
to the community. It requires the operator of a network server to
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
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.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
that there is no warranty for this free software. For both users' and
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
changed, so that their problems will not be attributed erroneously to
|
||||||
this license.
|
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
|
The precise terms and conditions for copying, distribution and
|
||||||
modification follow.
|
modification follow.
|
||||||
@@ -60,7 +72,7 @@ modification follow.
|
|||||||
|
|
||||||
0. Definitions.
|
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
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
works, such as semiconductor masks.
|
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
|
the Program, the only way you could satisfy both those terms and this
|
||||||
License would be to refrain entirely from conveying the Program.
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
13. Use with the GNU Affero 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.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
Notwithstanding any other provision of this License, you have
|
||||||
permission to link or combine any covered work with a work licensed
|
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
|
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,
|
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
|
but the special requirements of the GNU Affero General Public License,
|
||||||
3 of the GNU General Public License.
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
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
|
the GNU General Public License from time to time. Such new versions will
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
address new problems or concerns.
|
address new problems or concerns.
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
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
|
Public License "or any later version" applies to it, you have the
|
||||||
option of following the terms and conditions either of that numbered
|
option of following the terms and conditions either of that numbered
|
||||||
version or of any later version published by the Free Software
|
version or of any later version published by the Free Software
|
||||||
Foundation. If the Program does not specify a version number of the
|
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.
|
by the Free Software Foundation.
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
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
|
public statement of acceptance of a version permanently authorizes you
|
||||||
to choose that version for the Program.
|
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>
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
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/>.
|
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.
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
If the program does terminal interaction, make it output a short
|
||||||
network, you should also make sure that it provides a way for users to
|
notice like this when it starts in an interactive mode:
|
||||||
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
|
<program> Copyright (C) <year> <name of author>
|
||||||
of the code. There are many ways you could offer source, and different
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
solutions will be better for different programs; see section 13 for the
|
This is free software, and you are welcome to redistribute it
|
||||||
specific requirements.
|
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,
|
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.
|
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/>.
|
<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
|
Mgmt
|
||||||
Copyright (C) 2013-2016+ James Shubin and the project contributors
|
Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
Written by James Shubin <james@shubin.ca> 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
|
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
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
(at your option) any later version.
|
(at your option) any later version.
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
This program is distributed in the hope that it will be useful,
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
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/>.
|
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/)
|
|
||||||
214
Makefile
214
Makefile
@@ -1,28 +1,31 @@
|
|||||||
# Mgmt
|
# Mgmt
|
||||||
# Copyright (C) 2013-2016+ James Shubin and the project contributors
|
# Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
# Written by James Shubin <james@shubin.ca> 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
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# This program is distributed in the hope that it will be useful,
|
# This program is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
SHELL = /usr/bin/env bash
|
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 bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr tag release
|
||||||
.SILENT: clean
|
.SILENT: clean bindata
|
||||||
|
|
||||||
|
# a large amount of output from this `find`, can cause `make` to be much slower!
|
||||||
|
GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
|
||||||
|
|
||||||
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
|
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))
|
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||||
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
|
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))
|
ifeq ($(VERSION),$(SVERSION))
|
||||||
RELEASE = 1
|
RELEASE = 1
|
||||||
else
|
else
|
||||||
@@ -37,11 +40,27 @@ RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
|||||||
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
||||||
SERVER = 'dl.fedoraproject.org'
|
SERVER = 'dl.fedoraproject.org'
|
||||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
|
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)
|
||||||
|
|
||||||
|
RPM_PKG = releases/$(VERSION)/rpm/mgmt-$(VERSION)-1.x86_64.rpm
|
||||||
|
DEB_PKG = releases/$(VERSION)/deb/mgmt_$(VERSION)_amd64.deb
|
||||||
|
PACMAN_PKG = releases/$(VERSION)/pacman/mgmt-$(VERSION)-1-x86_64.pkg.tar.xz
|
||||||
|
|
||||||
|
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
|
||||||
|
SHA256SUMS_ASC = $(SHA256SUMS).asc
|
||||||
|
|
||||||
|
default: build
|
||||||
|
|
||||||
#
|
#
|
||||||
# art
|
# 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:
|
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
|
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,69 +96,110 @@ art/mgmt_logo_white_wide.png: art/mgmt_logo_white_wide.svg
|
|||||||
all: docs $(PROGRAM).static
|
all: docs $(PROGRAM).static
|
||||||
|
|
||||||
# show the current version
|
# show the current version
|
||||||
version:
|
version: ## show the current version
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
program:
|
program: ## show the program name
|
||||||
@echo $(PROGRAM)
|
@echo $(PROGRAM)
|
||||||
|
|
||||||
path:
|
path: ## create working paths
|
||||||
./misc/make-path.sh
|
./misc/make-path.sh
|
||||||
|
|
||||||
deps:
|
deps: ## install system and golang dependencies
|
||||||
./misc/make-deps.sh
|
./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)"
|
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
|
# include race flag
|
||||||
race:
|
race:
|
||||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||||
|
|
||||||
|
# generate go files from non-go source
|
||||||
|
bindata: ## generate go files from non-go sources
|
||||||
|
@echo "Generating: bindata..."
|
||||||
|
$(MAKE) --quiet -C bindata
|
||||||
|
$(MAKE) --quiet -C lang/funcs
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
go generate
|
go generate
|
||||||
|
|
||||||
build: $(PROGRAM)
|
lang: ## generates the lexer/parser for the language frontend
|
||||||
|
@# recursively run make in child dir named lang
|
||||||
|
@echo "Generating: lang..."
|
||||||
|
$(MAKE) --quiet -C lang
|
||||||
|
|
||||||
$(PROGRAM): main.go
|
# build a `mgmt` binary for current host os/arch
|
||||||
@echo "Building: $(PROGRAM), version: $(SVERSION)..."
|
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
||||||
ifneq ($(OLDGOLANG),)
|
cp -a $< $@
|
||||||
@# 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
|
|
||||||
|
|
||||||
$(PROGRAM).static: main.go
|
$(PROGRAM).static: $(GO_FILES)
|
||||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||||
go generate
|
go generate
|
||||||
ifneq ($(OLDGOLANG),)
|
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);
|
||||||
@# 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
|
|
||||||
|
|
||||||
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) | bindata lang
|
||||||
|
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
||||||
|
@# reassigning GOOS and GOARCH to make build command copy/pastable
|
||||||
|
@# go 1.10 requires specifying the package for ldflags
|
||||||
|
@if go version | grep -qE 'go1.9'; then \
|
||||||
|
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
|
||||||
|
else \
|
||||||
|
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# create a list of binary file names to use as make targets
|
||||||
|
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||||
|
crossbuild: ${crossbuild_targets}
|
||||||
|
|
||||||
|
clean: ## clean things up
|
||||||
|
$(MAKE) --quiet -C bindata clean
|
||||||
|
$(MAKE) --quiet -C lang/funcs clean
|
||||||
|
$(MAKE) --quiet -C lang clean
|
||||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||||
rm -f *_stringer.go # generated by `go generate`
|
rm -f *_stringer.go # generated by `go generate`
|
||||||
rm -f *_mock.go # generated by `go generate`
|
rm -f *_mock.go # generated by `go generate`
|
||||||
|
# crossbuild artifacts
|
||||||
|
rm -f build/mgmt-*
|
||||||
|
|
||||||
test:
|
test: build ## run tests
|
||||||
./test.sh
|
./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:
|
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 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \;
|
||||||
|
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \;
|
||||||
|
|
||||||
yamlfmt:
|
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" \;
|
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
|
$(PROGRAM)-documentation.pdf: docs/documentation.md
|
||||||
pandoc DOCUMENTATION.md -o '$(PROGRAM)-documentation.pdf'
|
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
|
||||||
|
|
||||||
#
|
#
|
||||||
# build aliases
|
# build aliases
|
||||||
@@ -161,7 +221,7 @@ rpmbuild/SOURCES/: tar
|
|||||||
rpmbuild/SRPMS/: srpm
|
rpmbuild/SRPMS/: srpm
|
||||||
rpmbuild/RPMS/: rpm
|
rpmbuild/RPMS/: rpm
|
||||||
|
|
||||||
upload: upload-sources upload-srpms upload-rpms
|
upload: upload-sources upload-srpms upload-rpms ## upload sources
|
||||||
# do nothing
|
# do nothing
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -183,7 +243,7 @@ $(SRPM): $(SPEC) $(SOURCE)
|
|||||||
#
|
#
|
||||||
$(SPEC): rpmbuild/ spec.in
|
$(SPEC): rpmbuild/ spec.in
|
||||||
@echo Running templater...
|
@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)
|
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
|
# 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)
|
git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC)
|
||||||
@@ -269,7 +329,83 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
|
|||||||
#
|
#
|
||||||
# copr build
|
# copr build
|
||||||
#
|
#
|
||||||
copr: upload-srpms
|
copr: upload-srpms ## build in copr
|
||||||
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
|
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
|
||||||
|
|
||||||
|
#
|
||||||
|
# tag
|
||||||
|
#
|
||||||
|
tag: ## tags a new release
|
||||||
|
./misc/tag.sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# release
|
||||||
|
#
|
||||||
|
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
|
||||||
|
|
||||||
|
releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA256SUMS_ASC)
|
||||||
|
@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 $(RPM_PKG) \
|
||||||
|
-a $(DEB_PKG) \
|
||||||
|
-a $(PACMAN_PKG) \
|
||||||
|
-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)/{deb,rpm,pacman}/ && touch releases/$(VERSION)/.mkdir
|
||||||
|
|
||||||
|
releases/$(VERSION)/rpm/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@echo "Generating rpm changelog..."
|
||||||
|
./misc/make-rpm-changelog.sh $(VERSION)
|
||||||
|
|
||||||
|
$(RPM_PKG): releases/$(VERSION)/rpm/changelog
|
||||||
|
@echo "Building rpm package..."
|
||||||
|
./misc/fpm-pack.sh rpm $(VERSION) libvirt-devel augeas-devel
|
||||||
|
|
||||||
|
releases/$(VERSION)/deb/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@echo "Generating deb changelog..."
|
||||||
|
./misc/make-deb-changelog.sh $(VERSION)
|
||||||
|
|
||||||
|
$(DEB_PKG): releases/$(VERSION)/deb/changelog
|
||||||
|
@echo "Building deb package..."
|
||||||
|
./misc/fpm-pack.sh deb $(VERSION) libvirt-dev libaugeas-dev
|
||||||
|
|
||||||
|
$(PACMAN_PKG): $(PROGRAM) releases/$(VERSION)/.mkdir
|
||||||
|
@echo "Building pacman package..."
|
||||||
|
./misc/fpm-pack.sh pacman $(VERSION) libvirt augeas
|
||||||
|
|
||||||
|
$(SHA256SUMS): $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG)
|
||||||
|
@# remove the directory separator in the SHA256SUMS file
|
||||||
|
@echo "Generating sha256 sum..."
|
||||||
|
sha256sum $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) | 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 ''
|
||||||
|
|
||||||
# vim: ts=8
|
# vim: ts=8
|
||||||
|
|||||||
147
README.md
147
README.md
@@ -2,112 +2,85 @@
|
|||||||
|
|
||||||
[](art/)
|
[](art/)
|
||||||
|
|
||||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||||
[](http://travis-ci.org/purpleidea/mgmt)
|
[](http://travis-ci.org/purpleidea/mgmt)
|
||||||
[](DOCUMENTATION.md)
|
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
[](https://www.patreon.com/purpleidea)
|
||||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
[](https://liberapay.com/purpleidea/donate)
|
||||||
[](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
|
||||||
[](https://aur.archlinux.org/packages/mgmt/)
|
|
||||||
|
|
||||||
## Community:
|
## 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://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
|
||||||
|
| 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:
|
## 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:
|
Mgmt is a next generation automation tool. It has similarities to other tools in
|
||||||
Please join the [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) IRC community!
|
the configuration management space, but has a fast, modern, distributed systems
|
||||||
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!
|
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:
|
Mgmt is a fairly new project. It is usable today, but not yet feature complete.
|
||||||
* Make sure you have golang version 1.6 or greater installed.
|
With your help you'll be able to influence our design and get us to 1.0 sooner!
|
||||||
* If you do not have a GOPATH yet, create one and export it:
|
Interested developers should read the [quick start guide](docs/quick-start-guide.md).
|
||||||
```
|
|
||||||
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!
|
|
||||||
|
|
||||||
## Documentation:
|
## 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).
|
|
||||||
|
Please read, enjoy and help improve our documentation!
|
||||||
|
|
||||||
|
| Documentation | Additional Notes |
|
||||||
|
|---|---|
|
||||||
|
| [quick start guide](docs/quick-start-guide.md) | for mgmt developers |
|
||||||
|
| [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 |
|
||||||
|
|
||||||
|
## 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!
|
||||||
|
|
||||||
## Roadmap:
|
## Roadmap:
|
||||||
|
|
||||||
|
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
issues if you're a first time contributor to the project or if you're unsure
|
||||||
|
about what to hack on!
|
||||||
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
|
Please 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!
|
Please get involved by working on one of these items or by suggesting something
|
||||||
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!
|
else!
|
||||||
|
|
||||||
## Bugs:
|
## 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:
|
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go)
|
||||||
* golang 1.6 or higher (required, available in most distros)
|
to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
|
||||||
* golang libraries (required, available with `go get`)
|
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
|
||||||
go get github.com/coreos/etcd/client
|
test case.
|
||||||
go get gopkg.in/yaml.v2
|
Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/).
|
||||||
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)
|
|
||||||
|
|
||||||
## Patches:
|
## Patches:
|
||||||
|
|
||||||
We'd love to have your patches! Please send them by email, or as a pull request.
|
We'd love to have your patches! Please send them by email, or as a pull request.
|
||||||
|
|
||||||
## On the web:
|
## 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/)
|
|
||||||
|
|
||||||
##
|
[Read what people are saying and publishing about mgmt!](docs/on-the-web.md)
|
||||||
|
|
||||||
Happy hacking!
|
Happy hacking!
|
||||||
|
|||||||
40
TODO.md
40
TODO.md
@@ -1,66 +1,84 @@
|
|||||||
# TODO
|
# TODO
|
||||||
|
|
||||||
If you're looking for something to do, look here!
|
If you're looking for something to do, look here!
|
||||||
Let us know if you're working on one of the items.
|
Let us know if you're working on one of the items.
|
||||||
|
If you'd like something to work on, ping @purpleidea and I'll create an issue
|
||||||
|
tailored especially for you! Just let me know your approximate golang skill
|
||||||
|
level and how many hours you'd like to spend on the patch.
|
||||||
|
|
||||||
## Package resource
|
## Package resource
|
||||||
|
|
||||||
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
|
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
|
||||||
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
|
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
|
||||||
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
|
- [ ] 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)
|
## File resource [bug](https://github.com/purpleidea/mgmt/issues/64) [: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)
|
|
||||||
- [ ] recurse limit support [: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)
|
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
|
||||||
|
|
||||||
## Svc resource
|
## Svc resource
|
||||||
|
|
||||||
- [ ] base resource improvements
|
- [ ] base resource improvements
|
||||||
|
|
||||||
## Exec resource
|
## Exec resource
|
||||||
|
|
||||||
- [ ] base resource improvements
|
- [ ] base resource improvements
|
||||||
|
|
||||||
## Timer resource
|
## Timer resource
|
||||||
- [ ] reset on recompile
|
|
||||||
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## User/Group resource
|
## 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)
|
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Virt (libvirt) resource
|
## Virt (libvirt) resource
|
||||||
- [ ] base resource [bug](https://github.com/purpleidea/mgmt/issues/25)
|
|
||||||
|
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Net (systemd-networkd) resource
|
## Net (systemd-networkd) resource
|
||||||
|
|
||||||
- [ ] base resource
|
- [ ] base resource
|
||||||
|
|
||||||
## Nspawn (systemd-nspawn) resource
|
## Nspawn (systemd-nspawn) resource
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Mount (systemd-mount) resource
|
## Mount (systemd-mount) resource
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Cron (systemd-timer) resource
|
## Cron (systemd-timer) resource
|
||||||
|
|
||||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Http resource
|
## Http resource
|
||||||
- [ ] base resource
|
|
||||||
|
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||||
|
|
||||||
## Etcd improvements
|
## Etcd improvements
|
||||||
|
|
||||||
- [ ] fix embedded etcd master race
|
- [ ] fix embedded etcd master race
|
||||||
|
|
||||||
## Torrent/dht file transfer
|
## Torrent/dht file transfer
|
||||||
|
|
||||||
|
- [ ] base plumbing
|
||||||
|
|
||||||
|
## GPG/Auth improvements
|
||||||
|
|
||||||
- [ ] base plumbing
|
- [ ] base plumbing
|
||||||
|
|
||||||
## Language improvements
|
## Language improvements
|
||||||
- [ ] language design
|
|
||||||
- [ ] lexer/parser
|
- [ ] more core functions
|
||||||
- [ ] automatic language formatter, ala `gofmt`
|
- [ ] automatic language formatter, ala `gofmt`
|
||||||
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
|
||||||
- [ ] vim syntax highlighting
|
- [ ] vim syntax highlighting
|
||||||
- [ ] emacs syntax highlighting
|
- [x] emacs syntax highlighting: see `misc/emacs/`
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
- [ ] better error/retry handling
|
- [ ] better error/retry handling
|
||||||
- [ ] deb package target in Makefile
|
- [ ] deb package target in Makefile
|
||||||
- [ ] reproducible builds
|
- [ ] reproducible builds
|
||||||
|
|||||||
50
Vagrantfile
vendored
Normal file
50
Vagrantfile
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
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 = "fedora/28-cloud-base"
|
||||||
|
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"
|
||||||
|
|
||||||
|
# copied from make-deps.sh (with added git)
|
||||||
|
config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make gem"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
go get -u github.com/purpleidea/mgmt
|
||||||
|
cd ~/gopath/src/github.com/purpleidea/mgmt
|
||||||
|
make deps
|
||||||
|
SCRIPT
|
||||||
|
config.vm.provision "shell" do |shell|
|
||||||
|
shell.privileged = false
|
||||||
|
shell.inline = script
|
||||||
|
end
|
||||||
|
end
|
||||||
41
bindata/Makefile
Normal file
41
bindata/Makefile
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Mgmt
|
||||||
|
# Copyright (C) 2013-2018+ 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/>.
|
||||||
|
|
||||||
|
# The bindata target generates go files from any source defined below. To use
|
||||||
|
# the files, import the generated "bindata" package and use:
|
||||||
|
# `bytes, err := bindata.Asset("FILEPATH")`
|
||||||
|
# where FILEPATH is the path of the original input file relative to `bindata/`.
|
||||||
|
# To get a list of files stored in this "bindata" package, you can use:
|
||||||
|
# `paths := bindata.AssetNames()` and `paths, err := bindata.AssetDir(name)`
|
||||||
|
# to get a list of files with a directory prefix.
|
||||||
|
|
||||||
|
.PHONY: build clean
|
||||||
|
default: build
|
||||||
|
|
||||||
|
build: bindata.go
|
||||||
|
|
||||||
|
# add more input files as dependencies at the end here...
|
||||||
|
bindata.go: ../COPYING
|
||||||
|
# go-bindata --pkg bindata -o <OUTPUT> <INPUT>
|
||||||
|
go-bindata --pkg bindata -o ./$@ $^
|
||||||
|
# gofmt the output file
|
||||||
|
gofmt -s -w $@
|
||||||
|
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@'
|
||||||
|
|
||||||
|
clean:
|
||||||
|
# remove generated bindata.go
|
||||||
|
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f bindata.go
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package converger is a facility for reporting the converged state.
|
// Package converger is a facility for reporting the converged state.
|
||||||
@@ -20,33 +20,37 @@ package converger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: we could make a new function that masks out the state of certain
|
// 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...
|
// UID's, but at the moment the new Timer code has obsoleted the need...
|
||||||
|
|
||||||
// Converger is the general interface for implementing a convergence watcher
|
// Converger is the general interface for implementing a convergence watcher.
|
||||||
type Converger interface { // TODO: need a better name
|
type Converger interface { // TODO: need a better name
|
||||||
Register() ConvergerUID
|
Register() UID
|
||||||
IsConverged(ConvergerUID) bool // is the UID converged ?
|
IsConverged(UID) bool // is the UID converged ?
|
||||||
SetConverged(ConvergerUID, bool) error // set the converged state of the UID
|
SetConverged(UID, bool) error // set the converged state of the UID
|
||||||
Unregister(ConvergerUID)
|
Unregister(UID)
|
||||||
Start()
|
Start()
|
||||||
Pause()
|
Pause()
|
||||||
Loop(bool)
|
Loop(bool)
|
||||||
ConvergedTimer(ConvergerUID) <-chan time.Time
|
ConvergedTimer(UID) <-chan time.Time
|
||||||
Status() map[uint64]bool
|
Status() map[uint64]bool
|
||||||
Timeout() int // returns the timeout that this was created with
|
Timeout() int // returns the timeout that this was created with
|
||||||
SetStateFn(func(bool) error) // sets the stateFn
|
AddStateFn(string, func(bool) error) error // adds a stateFn with a name
|
||||||
|
RemoveStateFn(string) error // remove a stateFn with a given name
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvergerUID is the interface resources can use to notify with if converged
|
// UID is the interface resources can use to notify with if converged. You'll
|
||||||
// you'll need to use part of the Converger interface to Register initially too
|
// need to use part of the Converger interface to Register initially too.
|
||||||
type ConvergerUID interface {
|
type UID interface {
|
||||||
ID() uint64 // get Id
|
ID() uint64 // get Id
|
||||||
Name() string // get a friendly name
|
Name() string // get a friendly name
|
||||||
SetName(string)
|
SetName(string)
|
||||||
@@ -61,77 +65,83 @@ type ConvergerUID interface {
|
|||||||
StopTimer() error
|
StopTimer() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// converger is an implementation of the Converger interface
|
// converger is an implementation of the Converger interface.
|
||||||
type converger struct {
|
type converger struct {
|
||||||
timeout int // must be zero (instant) or greater seconds to run
|
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)
|
||||||
converged bool // did we converge (state changes of this run Fn)
|
channel chan struct{} // signal here to run an isConverged check
|
||||||
channel chan struct{} // signal here to run an isConverged check
|
control chan bool // control channel for start/pause
|
||||||
control chan bool // control channel for start/pause
|
mutex *sync.RWMutex // used for controlling access to status and lastid
|
||||||
mutex sync.RWMutex // used for controlling access to status and lastid
|
|
||||||
lastid uint64
|
lastid uint64
|
||||||
status map[uint64]bool
|
status map[uint64]bool
|
||||||
|
stateFns map[string]func(bool) error // run on converged state changes with state bool
|
||||||
|
smutex *sync.RWMutex // used for controlling access to stateFns
|
||||||
}
|
}
|
||||||
|
|
||||||
// convergerUID is an implementation of the ConvergerUID interface
|
// cuid is an implementation of the UID interface.
|
||||||
type convergerUID struct {
|
type cuid struct {
|
||||||
converger Converger
|
converger Converger
|
||||||
id uint64
|
id uint64
|
||||||
name string // user defined, friendly name
|
name string // user defined, friendly name
|
||||||
mutex sync.Mutex
|
mutex *sync.Mutex
|
||||||
timer chan struct{}
|
timer chan struct{}
|
||||||
running bool // is the above timer running?
|
running bool // is the above timer running?
|
||||||
|
wg *sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConverger builds a new converger struct
|
// NewConverger builds a new converger struct.
|
||||||
func NewConverger(timeout int, stateFn func(bool) error) *converger {
|
func NewConverger(timeout int) Converger {
|
||||||
return &converger{
|
return &converger{
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
stateFn: stateFn,
|
channel: make(chan struct{}),
|
||||||
channel: make(chan struct{}),
|
control: make(chan bool),
|
||||||
control: make(chan bool),
|
mutex: &sync.RWMutex{},
|
||||||
lastid: 0,
|
lastid: 0,
|
||||||
status: make(map[uint64]bool),
|
status: make(map[uint64]bool),
|
||||||
|
stateFns: make(map[string]func(bool) error),
|
||||||
|
smutex: &sync.RWMutex{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register assigns a ConvergerUID to the caller
|
// Register assigns a UID to the caller.
|
||||||
func (obj *converger) Register() ConvergerUID {
|
func (obj *converger) Register() UID {
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
defer obj.mutex.Unlock()
|
defer obj.mutex.Unlock()
|
||||||
obj.lastid++
|
obj.lastid++
|
||||||
obj.status[obj.lastid] = false // initialize as not converged
|
obj.status[obj.lastid] = false // initialize as not converged
|
||||||
return &convergerUID{
|
return &cuid{
|
||||||
converger: obj,
|
converger: obj,
|
||||||
id: obj.lastid,
|
id: obj.lastid,
|
||||||
name: fmt.Sprintf("%d", obj.lastid), // some default
|
name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||||
|
mutex: &sync.Mutex{},
|
||||||
timer: nil,
|
timer: nil,
|
||||||
running: false,
|
running: false,
|
||||||
|
wg: &sync.WaitGroup{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConverged gets the converged status of a uid
|
// IsConverged gets the converged status of a uid.
|
||||||
func (obj *converger) IsConverged(uid ConvergerUID) bool {
|
func (obj *converger) IsConverged(uid UID) bool {
|
||||||
if !uid.IsValid() {
|
if !uid.IsValid() {
|
||||||
panic(fmt.Sprintf("Id of ConvergerUID(%s) is nil!", uid.Name()))
|
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||||
}
|
}
|
||||||
obj.mutex.RLock()
|
obj.mutex.RLock()
|
||||||
isConverged, found := obj.status[uid.ID()] // lookup
|
isConverged, found := obj.status[uid.ID()] // lookup
|
||||||
obj.mutex.RUnlock()
|
obj.mutex.RUnlock()
|
||||||
if !found {
|
if !found {
|
||||||
panic("Id of ConvergerUID is unregistered!")
|
panic("the ID of UID is unregistered")
|
||||||
}
|
}
|
||||||
return isConverged
|
return isConverged
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConverged updates the converger with the converged state of the UID
|
// SetConverged updates the converger with the converged state of the UID.
|
||||||
func (obj *converger) SetConverged(uid ConvergerUID, isConverged bool) error {
|
func (obj *converger) SetConverged(uid UID, isConverged bool) error {
|
||||||
if !uid.IsValid() {
|
if !uid.IsValid() {
|
||||||
return fmt.Errorf("Id of ConvergerUID(%s) is nil!", uid.Name())
|
return fmt.Errorf("the ID of UID(%s) is nil", uid.Name())
|
||||||
}
|
}
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
if _, found := obj.status[uid.ID()]; !found {
|
if _, found := obj.status[uid.ID()]; !found {
|
||||||
panic("Id of ConvergerUID is unregistered!")
|
panic("the ID of UID is unregistered")
|
||||||
}
|
}
|
||||||
obj.status[uid.ID()] = isConverged // set
|
obj.status[uid.ID()] = isConverged // set
|
||||||
obj.mutex.Unlock() // unlock *before* poke or deadlock!
|
obj.mutex.Unlock() // unlock *before* poke or deadlock!
|
||||||
@@ -143,7 +153,7 @@ func (obj *converger) SetConverged(uid ConvergerUID, isConverged bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isConverged returns true if *every* registered uid has converged
|
// isConverged returns true if *every* registered uid has converged.
|
||||||
func (obj *converger) isConverged() bool {
|
func (obj *converger) isConverged() bool {
|
||||||
obj.mutex.RLock() // take a read lock
|
obj.mutex.RLock() // take a read lock
|
||||||
defer obj.mutex.RUnlock()
|
defer obj.mutex.RUnlock()
|
||||||
@@ -155,10 +165,10 @@ func (obj *converger) isConverged() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister dissociates the ConvergedUID from the converged checking
|
// Unregister dissociates the ConvergedUID from the converged checking.
|
||||||
func (obj *converger) Unregister(uid ConvergerUID) {
|
func (obj *converger) Unregister(uid UID) {
|
||||||
if !uid.IsValid() {
|
if !uid.IsValid() {
|
||||||
panic(fmt.Sprintf("Id of ConvergerUID(%s) is nil!", uid.Name()))
|
panic(fmt.Sprintf("the ID of UID(%s) is nil", uid.Name()))
|
||||||
}
|
}
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
uid.StopTimer() // ignore any errors
|
uid.StopTimer() // ignore any errors
|
||||||
@@ -167,30 +177,30 @@ func (obj *converger) Unregister(uid ConvergerUID) {
|
|||||||
uid.InvalidateID()
|
uid.InvalidateID()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start causes a Converger object to start or resume running
|
// Start causes a Converger object to start or resume running.
|
||||||
func (obj *converger) Start() {
|
func (obj *converger) Start() {
|
||||||
obj.control <- true
|
obj.control <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pause causes a Converger object to stop running temporarily
|
// Pause causes a Converger object to stop running temporarily.
|
||||||
func (obj *converger) Pause() { // FIXME: add a sync ACK on pause before return
|
func (obj *converger) Pause() { // FIXME: add a sync ACK on pause before return
|
||||||
obj.control <- false
|
obj.control <- false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop is the main loop for a Converger object; it usually runs in a goroutine
|
// 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
|
// 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
|
// 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
|
// 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!
|
// have joined the map, then it might appear as if we converged before we did!
|
||||||
func (obj *converger) Loop(startPaused bool) {
|
func (obj *converger) Loop(startPaused bool) {
|
||||||
if obj.control == nil {
|
if obj.control == nil {
|
||||||
panic("Converger not initialized correctly")
|
panic("converger not initialized correctly")
|
||||||
}
|
}
|
||||||
if startPaused { // start paused without racing
|
if startPaused { // start paused without racing
|
||||||
select {
|
select {
|
||||||
case e := <-obj.control:
|
case e := <-obj.control:
|
||||||
if !e {
|
if !e {
|
||||||
panic("Converger expected true!")
|
panic("converger expected true")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,13 +208,13 @@ func (obj *converger) Loop(startPaused bool) {
|
|||||||
select {
|
select {
|
||||||
case e := <-obj.control: // expecting "false" which means pause!
|
case e := <-obj.control: // expecting "false" which means pause!
|
||||||
if e {
|
if e {
|
||||||
panic("Converger expected false!")
|
panic("converger expected false")
|
||||||
}
|
}
|
||||||
// now i'm paused...
|
// now i'm paused...
|
||||||
select {
|
select {
|
||||||
case e := <-obj.control:
|
case e := <-obj.control:
|
||||||
if !e {
|
if !e {
|
||||||
panic("Converger expected true!")
|
panic("converger expected true")
|
||||||
}
|
}
|
||||||
// restart
|
// restart
|
||||||
// kick once to refresh the check...
|
// kick once to refresh the check...
|
||||||
@@ -215,11 +225,9 @@ func (obj *converger) Loop(startPaused bool) {
|
|||||||
case <-obj.channel:
|
case <-obj.channel:
|
||||||
if !obj.isConverged() {
|
if !obj.isConverged() {
|
||||||
if obj.converged { // we're doing a state change
|
if obj.converged { // we're doing a state change
|
||||||
if obj.stateFn != nil {
|
// call the arbitrary functions (takes a read lock!)
|
||||||
// call an arbitrary function
|
if err := obj.runStateFns(false); err != nil {
|
||||||
if err := obj.stateFn(false); err != nil {
|
// FIXME: what to do on error ?
|
||||||
// FIXME: what to do on error ?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
obj.converged = false
|
obj.converged = false
|
||||||
@@ -229,11 +237,9 @@ func (obj *converger) Loop(startPaused bool) {
|
|||||||
// we have converged!
|
// we have converged!
|
||||||
if obj.timeout >= 0 { // only run if timeout is valid
|
if obj.timeout >= 0 { // only run if timeout is valid
|
||||||
if !obj.converged { // we're doing a state change
|
if !obj.converged { // we're doing a state change
|
||||||
if obj.stateFn != nil {
|
// call the arbitrary functions (takes a read lock!)
|
||||||
// call an arbitrary function
|
if err := obj.runStateFns(true); err != nil {
|
||||||
if err := obj.stateFn(true); err != nil {
|
// FIXME: what to do on error ?
|
||||||
// FIXME: what to do on error ?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -243,9 +249,9 @@ func (obj *converger) Loop(startPaused bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvergedTimer adds a timeout to a select call and blocks until then
|
// ConvergedTimer adds a timeout to a select call and blocks until then.
|
||||||
// TODO: this means we could eventually have per resource converged timeouts
|
// TODO: this means we could eventually have per resource converged timeouts
|
||||||
func (obj *converger) ConvergedTimer(uid ConvergerUID) <-chan time.Time {
|
func (obj *converger) ConvergedTimer(uid UID) <-chan time.Time {
|
||||||
// be clever: if i'm already converged, this timeout should block which
|
// be clever: if i'm already converged, this timeout should block which
|
||||||
// avoids unnecessary new signals being sent! this avoids fast loops if
|
// avoids unnecessary new signals being sent! this avoids fast loops if
|
||||||
// we have a low timeout, or in particular a timeout == 0
|
// we have a low timeout, or in particular a timeout == 0
|
||||||
@@ -274,68 +280,107 @@ func (obj *converger) Timeout() int {
|
|||||||
return obj.timeout
|
return obj.timeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetStateFn sets the state function to be run on change of converged state.
|
// AddStateFn adds a state function to be run on change of converged state.
|
||||||
func (obj *converger) SetStateFn(stateFn func(bool) error) {
|
func (obj *converger) AddStateFn(name string, stateFn func(bool) error) error {
|
||||||
obj.stateFn = stateFn
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// Id returns the unique id of this UID object
|
// RemoveStateFn adds a state function to be run on change of converged state.
|
||||||
func (obj *convergerUID) ID() uint64 {
|
func (obj *converger) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// runStateFns runs the listed of stored state functions.
|
||||||
|
func (obj *converger) runStateFns(converged bool) error {
|
||||||
|
obj.smutex.RLock()
|
||||||
|
defer obj.smutex.RUnlock()
|
||||||
|
var keys []string
|
||||||
|
for k := range obj.stateFns {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
var err error
|
||||||
|
for _, name := range keys { // run in deterministic order
|
||||||
|
fn := obj.stateFns[name]
|
||||||
|
// call an arbitrary function
|
||||||
|
if e := fn(converged); e != nil {
|
||||||
|
err = multierr.Append(err, e) // list of errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the unique id of this UID object.
|
||||||
|
func (obj *cuid) ID() uint64 {
|
||||||
return obj.id
|
return obj.id
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name returns a user defined name for the specific convergerUID.
|
// Name returns a user defined name for the specific cuid.
|
||||||
func (obj *convergerUID) Name() string {
|
func (obj *cuid) Name() string {
|
||||||
return obj.name
|
return obj.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetName sets a user defined name for the specific convergerUID.
|
// SetName sets a user defined name for the specific cuid.
|
||||||
func (obj *convergerUID) SetName(name string) {
|
func (obj *cuid) SetName(name string) {
|
||||||
obj.name = name
|
obj.name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsValid tells us if the id is valid or has already been destroyed
|
// IsValid tells us if the id is valid or has already been destroyed.
|
||||||
func (obj *convergerUID) IsValid() bool {
|
func (obj *cuid) IsValid() bool {
|
||||||
return obj.id != 0 // an id of 0 is invalid
|
return obj.id != 0 // an id of 0 is invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateID marks the id as no longer valid
|
// InvalidateID marks the id as no longer valid.
|
||||||
func (obj *convergerUID) InvalidateID() {
|
func (obj *cuid) InvalidateID() {
|
||||||
obj.id = 0 // an id of 0 is invalid
|
obj.id = 0 // an id of 0 is invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsConverged is a helper function to the regular IsConverged method
|
// IsConverged is a helper function to the regular IsConverged method.
|
||||||
func (obj *convergerUID) IsConverged() bool {
|
func (obj *cuid) IsConverged() bool {
|
||||||
return obj.converger.IsConverged(obj)
|
return obj.converger.IsConverged(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConverged is a helper function to the regular SetConverged notification
|
// SetConverged is a helper function to the regular SetConverged notification.
|
||||||
func (obj *convergerUID) SetConverged(isConverged bool) error {
|
func (obj *cuid) SetConverged(isConverged bool) error {
|
||||||
return obj.converger.SetConverged(obj, isConverged)
|
return obj.converger.SetConverged(obj, isConverged)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister is a helper function to unregister myself
|
// Unregister is a helper function to unregister myself.
|
||||||
func (obj *convergerUID) Unregister() {
|
func (obj *cuid) Unregister() {
|
||||||
obj.converger.Unregister(obj)
|
obj.converger.Unregister(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvergedTimer is a helper around the regular ConvergedTimer method
|
// ConvergedTimer is a helper around the regular ConvergedTimer method.
|
||||||
func (obj *convergerUID) ConvergedTimer() <-chan time.Time {
|
func (obj *cuid) ConvergedTimer() <-chan time.Time {
|
||||||
return obj.converger.ConvergedTimer(obj)
|
return obj.converger.ConvergedTimer(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartTimer runs an invisible timer that automatically converges on timeout.
|
// StartTimer runs an invisible timer that automatically converges on timeout.
|
||||||
func (obj *convergerUID) StartTimer() (func() error, error) {
|
func (obj *cuid) StartTimer() (func() error, error) {
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
if !obj.running {
|
if !obj.running {
|
||||||
obj.timer = make(chan struct{})
|
obj.timer = make(chan struct{})
|
||||||
obj.running = true
|
obj.running = true
|
||||||
} else {
|
} else {
|
||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
return obj.StopTimer, fmt.Errorf("Timer already started!")
|
return obj.StopTimer, fmt.Errorf("timer already started")
|
||||||
}
|
}
|
||||||
obj.mutex.Unlock()
|
obj.mutex.Unlock()
|
||||||
|
obj.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer obj.wg.Done()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case _, ok := <-obj.timer: // reset signal channel
|
case _, ok := <-obj.timer: // reset signal channel
|
||||||
@@ -359,24 +404,25 @@ func (obj *convergerUID) StartTimer() (func() error, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
||||||
func (obj *convergerUID) ResetTimer() error {
|
func (obj *cuid) ResetTimer() error {
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
defer obj.mutex.Unlock()
|
defer obj.mutex.Unlock()
|
||||||
if obj.running {
|
if obj.running {
|
||||||
obj.timer <- struct{}{} // send the reset message
|
obj.timer <- struct{}{} // send the reset message
|
||||||
return nil
|
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.
|
// StopTimer stops the running timer permanently until a StartTimer is run.
|
||||||
func (obj *convergerUID) StopTimer() error {
|
func (obj *cuid) StopTimer() error {
|
||||||
obj.mutex.Lock()
|
obj.mutex.Lock()
|
||||||
defer obj.mutex.Unlock()
|
defer obj.mutex.Unlock()
|
||||||
if !obj.running {
|
if !obj.running {
|
||||||
return fmt.Errorf("Timer isn't running!")
|
return fmt.Errorf("timer isn't running")
|
||||||
}
|
}
|
||||||
close(obj.timer)
|
close(obj.timer)
|
||||||
|
obj.wg.Wait()
|
||||||
obj.running = false
|
obj.running = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
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-2018+ 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
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package main provides the main entrypoint for using the `mgmt` software.
|
// Package main provides the main entrypoint for using the `mgmt` software.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM golang:1.6.2
|
FROM golang:1.9
|
||||||
|
|
||||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||||
|
|
||||||
# Set the reset cache variable
|
# 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
|
# 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 2017-11-16
|
||||||
|
|
||||||
# Update the package list to be able to use required packages
|
# Update the package list to be able to use required packages
|
||||||
RUN apt-get update
|
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.9.1.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.9.1.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.9
|
||||||
|
|
||||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||||
|
|
||||||
# Set the reset cache variable
|
# 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
|
# 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 2017-11-16
|
||||||
|
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
|
|
||||||
@@ -27,5 +27,8 @@ WORKDIR /home/$USER_NAME/mgmt
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN make deps
|
RUN make deps
|
||||||
|
|
||||||
|
# Chown $GOPATH
|
||||||
|
RUN chown -R ${USER_ID}:${GROUP_ID} /go
|
||||||
|
|
||||||
# Change user
|
# Change user
|
||||||
USER ${USER_NAME}
|
USER ${USER_NAME}
|
||||||
|
|||||||
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-2018+ 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'),
|
||||||
|
]
|
||||||
49
docs/development.md
Normal file
49
docs/development.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
This document contains some additional information and help regarding
|
||||||
|
developing `mgmt`. Useful tools, conventions, etc.
|
||||||
|
|
||||||
|
Be sure to read [quick start guide](docs/quick-start-guide.md) first.
|
||||||
|
|
||||||
|
## 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)
|
||||||
466
docs/documentation.md
Normal file
466
docs/documentation.md
Normal file
@@ -0,0 +1,466 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
#### 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.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/).
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
#### `--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.
|
||||||
|
|
||||||
|
#### `--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`.
|
||||||
|
|
||||||
|
#### `--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.
|
||||||
|
|
||||||
|
### 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-2018+ 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/)
|
||||||
270
docs/faq.md
Normal file
270
docs/faq.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
## 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!
|
||||||
|
|
||||||
|
### 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://webchat.freenode.net/?channels=#mgmtconfig)
|
||||||
|
IRC channel on the [Freenode](https://freenode.net/) network. You can use any
|
||||||
|
IRC client that you'd like, but the [hosted web portal](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||||
|
will suffice if you don't know what else to use.
|
||||||
|
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.
|
||||||
|
|
||||||
|
### 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 --lang examples/lang/hello0.mcl --converged-timeout=5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 `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.
|
||||||
|
|
||||||
|
### 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` or `godep` 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.
|
||||||
|
|
||||||
|
### 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://webchat.freenode.net/?channels=#mgmtconfig)
|
||||||
|
to see if someone can help you. Once we get a big enough community going, we'll
|
||||||
|
add a mailing list. If you don't get any response from the above, you can
|
||||||
|
contact me through my [technical blog](https://purpleidea.com/contact/)
|
||||||
|
and I'll do my best to help. If you have a good question, please add it as a
|
||||||
|
patch to this documentation. I'll merge your question, and add a patch with the
|
||||||
|
answer!
|
||||||
446
docs/function-guide.md
Normal file
446
docs/function-guide.md
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
# 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 in
|
||||||
|
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/).
|
||||||
|
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/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// you must register your functions in init when the program starts up
|
||||||
|
func init() {
|
||||||
|
// Example function that squares an int and prints out answer as an str.
|
||||||
|
Register("talkingsquare", &types.FuncValue{
|
||||||
|
T: types.NewType("func(a int) str"), // declare the signature
|
||||||
|
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 in
|
||||||
|
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/).
|
||||||
|
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.
|
||||||
|
|
||||||
|
An example explains it best:
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/lang/types"
|
||||||
|
"github.com/purpleidea/mgmt/lang/funcs/simplepoly"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Default
|
||||||
|
|
||||||
|
```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 poiinters 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 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.
|
||||||
|
|
||||||
|
### 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
|
||||||
910
docs/language-guide.md
Normal file
910
docs/language-guide.md
Normal file
@@ -0,0 +1,910 @@
|
|||||||
|
# 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 => "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 is:
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
#### 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!
|
||||||
46
docs/on-the-web.md
Normal file
46
docs/on-the-web.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# 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/) |
|
||||||
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
|
# Puppet guide
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
`mgmt` can use Puppet as its source for the configuration graph.
|
`mgmt` can use Puppet as its source for the configuration graph.
|
||||||
This document goes into detail on how this works, and lists
|
This document goes into detail on how this works, and lists
|
||||||
some pitfalls and limitations.
|
some pitfalls and limitations.
|
||||||
|
|
||||||
For basic instructions on how to use the Puppet support, see
|
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
|
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
|
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
|
`mgmt`. Most importantly, make sure to install the `ffrank-mgmtgraph` Puppet
|
||||||
module (referred to below as "the translator module").
|
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
|
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`.
|
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
|
You can install the module on the master anyway, so that it gets distributed
|
||||||
to your agents through Puppet's `pluginsync` mechanism.
|
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
|
The following command should run successfully and print a YAML hash on your
|
||||||
terminal:
|
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
|
You can use this CLI to test any manifests before handing them straight
|
||||||
to `mgmt`.
|
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
|
`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,
|
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
|
notion of permissions (the file `mode`) yet. This lead to the following
|
||||||
warning (among others that will be discussed below):
|
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
|
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
|
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
|
manifests that are written expressly for `mgmt` is not sensible and should
|
||||||
be avoided.
|
be avoided.
|
||||||
|
|
||||||
###Unsupported resources
|
### Unsupported resources
|
||||||
|
|
||||||
Puppet has a fairly large number of
|
Puppet has a fairly large number of
|
||||||
[built-in types](https://docs.puppet.com/puppet/latest/reference/type.html),
|
[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).
|
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,
|
Many resource parameters in Puppet take default values. For the most part,
|
||||||
the translator module just ignores them. However, there are cases in which
|
the translator module just ignores them. However, there are cases in which
|
||||||
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
|
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
|
||||||
For example, translating a plain `file` resource will lead to a warning message:
|
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
|
The reason is that per default, Puppet assumes the following parameter value
|
||||||
(among others)
|
(among others)
|
||||||
|
|
||||||
```puppet
|
```puppet
|
||||||
file { "/tmp/mgmt-test":
|
file { "/tmp/mgmt-test":
|
||||||
backup => 'puppet',
|
backup => 'puppet',
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
To avoid this, specify the parameter explicitly:
|
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
|
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)
|
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,
|
If you encounter similar warnings from other types and/or parameters,
|
||||||
use the same approach to silence them if possible.
|
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
|
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
|
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
|
you to specify which `puppet.conf` file should be used during
|
||||||
translation.
|
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
|
Within this file, you can just specify any needed options in the
|
||||||
`[main]` section:
|
`[main]` section:
|
||||||
|
|
||||||
[main]
|
```
|
||||||
server=mgmt-master.example.net
|
[main]
|
||||||
vardir=/var/lib/mgmt/puppet
|
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)
|
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
|
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
|
You should probably make sure to always use the latest release of
|
||||||
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
|
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
|
||||||
getting pulled in as a dependency of the former).
|
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`, `--puppet-conf`, and `--lang` for mcl,
|
||||||
|
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.
|
||||||
185
docs/quick-start-guide.md
Normal file
185
docs/quick-start-guide.md
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
# Quick start guide
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
This guide is intended for developers. Once `mgmt` is minimally viable, we'll
|
||||||
|
publish a quick start guide for users too. If you're brand new to `mgmt`, it's
|
||||||
|
probably a good idea to start by reading the
|
||||||
|
[introductory article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||||
|
or to watch an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1).
|
||||||
|
Once you're familiar with the general idea, please start hacking...
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
### Installing golang
|
||||||
|
|
||||||
|
* You need golang version 1.10 or greater installed.
|
||||||
|
* To install on rpm style systems: `sudo dnf install golang`
|
||||||
|
* To install on apt style systems: `sudo apt install golang`
|
||||||
|
* To install on macOS systems install [Homebrew](https://brew.sh)
|
||||||
|
and run: `brew install go`
|
||||||
|
* You can run `go version` to check the golang version.
|
||||||
|
* If your distro is tool old, you may need to [download](https://golang.org/dl/)
|
||||||
|
a newer golang version.
|
||||||
|
|
||||||
|
### Setting up golang
|
||||||
|
|
||||||
|
* 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).
|
||||||
|
|
||||||
|
### Getting the mgmt code and dependencies
|
||||||
|
|
||||||
|
* Download the `mgmt` code into the GOPATH, and switch to that directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p $GOPATH/src/github.com/purpleidea/
|
||||||
|
cd $GOPATH/src/github.com/purpleidea/
|
||||||
|
git clone --recursive https://github.com/purpleidea/mgmt/
|
||||||
|
cd $GOPATH/src/github.com/purpleidea/mgmt
|
||||||
|
```
|
||||||
|
|
||||||
|
* Add $GOPATH/bin to $PATH
|
||||||
|
|
||||||
|
```
|
||||||
|
export PATH=$PATH:$GOPATH/bin
|
||||||
|
```
|
||||||
|
|
||||||
|
* Run `make deps` to install system and golang dependencies. Take a look at
|
||||||
|
`misc/make-deps.sh` for details.
|
||||||
|
* Run `make build` to get a freshly built `mgmt` binary.
|
||||||
|
|
||||||
|
### Running mgmt
|
||||||
|
|
||||||
|
* Run `time ./mgmt run --tmp-prefix lang --lang examples/lang/hello0.mcl` to try
|
||||||
|
out a very simple example!
|
||||||
|
* Look in that example file that you ran to see if you can figure out what it
|
||||||
|
did!
|
||||||
|
* 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!
|
||||||
|
|
||||||
|
## Vagrant
|
||||||
|
|
||||||
|
If you would like to avoid doing the above steps manually, we have prepared a
|
||||||
|
[Vagrant](https://www.vagrantup.com/) environment for your convenience. From the
|
||||||
|
project directory, run a `vagrant up`, and then a `vagrant status`. From there,
|
||||||
|
you can `vagrant ssh` into the `mgmt` machine. The MOTD will explain the rest.
|
||||||
|
|
||||||
|
## Using Docker
|
||||||
|
|
||||||
|
Alternatively, you can check out the [docker-guide](docs/docker-guide.md) in
|
||||||
|
order to develop or deploy using docker.
|
||||||
|
|
||||||
|
## Information about dependencies
|
||||||
|
|
||||||
|
Software projects have a few different kinds of dependencies. There are _build_
|
||||||
|
dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
|
||||||
|
required for running the _test_ suite.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
* `golang` 1.10 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`
|
||||||
|
|
||||||
|
## Binary Package Installation
|
||||||
|
|
||||||
|
Installation of `mgmt` from distribution packages currently needs improvement.
|
||||||
|
They are not always up-to-date with git master and as such are not recommended.
|
||||||
|
At the moment we have:
|
||||||
|
* [COPR](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||||
|
* [Arch](https://aur.archlinux.org/packages/mgmt/)
|
||||||
|
|
||||||
|
Please contribute more! We'd especially like to see a Debian package!
|
||||||
|
|
||||||
|
## OSX/macOS/Darwin development
|
||||||
|
|
||||||
|
Developing and running `mgmt` on macOS is currently not supported (but not
|
||||||
|
discouraged either). Meaning it might work but in the case it doesn't you would
|
||||||
|
have to provide your own patches to fix problems (the project maintainer and
|
||||||
|
community are glad to assist where needed).
|
||||||
|
|
||||||
|
There are currently some issues that make `mgmt` less suitable to run for provisioning
|
||||||
|
macOS. But as a client to provision remote servers it should run fine.
|
||||||
|
|
||||||
|
Since the primary supported systems are Linux and these are the environments
|
||||||
|
tested for it is wise to run these suites during macOS development as well. To
|
||||||
|
ease this Docker can be leveraged ([Docker for Mac](https://docs.docker.com/docker-for-mac/)).
|
||||||
|
|
||||||
|
Before running any of the commands below create the development Docker image:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/scripts/build-development
|
||||||
|
```
|
||||||
|
|
||||||
|
This image requires updating every time dependencies (`make-deps.sh`) change.
|
||||||
|
|
||||||
|
Then to run the test suite:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --rm -ti \
|
||||||
|
-v $PWD:/go/src/github.com/purpleidea/mgmt/ \
|
||||||
|
-w /go/src/github.com/purpleidea/mgmt/ \
|
||||||
|
purpleidea/mgmt:development \
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience this command is wrapped in `docker/scripts/exec-development`.
|
||||||
|
|
||||||
|
Basically any command can be executed this way. Because the repository source is
|
||||||
|
mounted into the Docker container invocation will be quick and allow rapid
|
||||||
|
testing, example:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/scripts/exec-development test/test-shell.sh load0.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Other examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker/scripts/exec-development make build
|
||||||
|
docker/scripts/exec-development ./mgmt run --tmp-prefix lang --lang examples/lang/load0.mcl
|
||||||
|
```
|
||||||
808
docs/resource-guide.md
Normal file
808
docs/resource-guide.md
Normal file
@@ -0,0 +1,808 @@
|
|||||||
|
# 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 have the correct default as the golang zero value. In
|
||||||
|
general it is preferable if the zero values make for the correct defaults.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```golang
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *FooRes) Default() 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 process
|
||||||
|
events from the engine via the `<-obj.init.Events` channel, and receive events
|
||||||
|
for our resource itself!
|
||||||
|
|
||||||
|
#### Events
|
||||||
|
|
||||||
|
If we receive an internal event from the `<-obj.init.Events` channel, we should
|
||||||
|
read it with the `obj.init.Read` helper function. This function tells us if we
|
||||||
|
should shutdown our resource. It also handles pause functionality which blocks
|
||||||
|
our resource temporarily in this method. If this channel shuts down, then we
|
||||||
|
should treat that as an exit signal.
|
||||||
|
|
||||||
|
When we want to send an event, we use the `Event` helper function. It is also
|
||||||
|
important to mark the resource state as `dirty` if we believe it might have
|
||||||
|
changed. We do this by calling the `obj.init.Dirty` function.
|
||||||
|
|
||||||
|
#### 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. If it returns an error, you must exit and return that
|
||||||
|
error.
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
// shutdown engine
|
||||||
|
// (it is okay if some `defer` code runs first)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// the actual events!
|
||||||
|
case event := <-obj.foo.Events:
|
||||||
|
if is_an_event {
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
// event errors
|
||||||
|
case err := <-obj.foo.Errors:
|
||||||
|
return err // will cause a retry or permanent failure
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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`.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
Events is a channel that we must watch for messages from the engine. When it
|
||||||
|
closes, this is a signal to shutdown. It is
|
||||||
|
only called from within `Watch`.
|
||||||
|
|
||||||
|
### Read
|
||||||
|
|
||||||
|
Read processes messages that come in from the `Events` channel. It is a helper
|
||||||
|
method that knows how to handle the pause mechanism correctly. It is
|
||||||
|
only called from within `Watch`.
|
||||||
|
|
||||||
|
### Dirty
|
||||||
|
|
||||||
|
Dirty marks the resource state as dirty. This signals to the engine that
|
||||||
|
CheckApply will have some work to do in order to converge it. 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!
|
||||||
|
|
||||||
|
### 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!
|
||||||
219
docs/resources.md
Normal file
219
docs/resources.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# 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.
|
||||||
|
* [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.
|
||||||
|
* [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)
|
||||||
|
* `content`: raw file content
|
||||||
|
* `state`: either `exists` (the default value) or `absent`
|
||||||
|
* `mode`: octal unix file permissions
|
||||||
|
* `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.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
151
docs/style-guide.md
Normal file
151
docs/style-guide.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
### 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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
## 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!
|
||||||
125
engine/autoedge.go
Normal file
125
engine/autoedge.go
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIFF1(t *testing.T) {
|
||||||
|
uid := &BaseUID{Name: "/tmp/unit-test"}
|
||||||
|
same := &BaseUID{Name: "/tmp/unit-test"}
|
||||||
|
diff := &BaseUID{Name: "/tmp/other-file"}
|
||||||
|
|
||||||
|
if !uid.IFF(same) {
|
||||||
|
t.Errorf("basic resource UIDs with the same name should satisfy each other's IFF condition")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uid.IFF(diff) {
|
||||||
|
t.Errorf("basic resource UIDs with different names should NOT satisfy each other's IFF condition")
|
||||||
|
}
|
||||||
|
}
|
||||||
88
engine/autogroup.go
Normal file
88
engine/autogroup.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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
|
||||||
|
}
|
||||||
319
engine/cmp.go
Normal file
319
engine/cmp.go
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
160
engine/copy.go
Normal file
160
engine/copy.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
60
engine/edge.go
Normal file
60
engine/edge.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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
|
||||||
|
}
|
||||||
32
engine/error.go
Normal file
32
engine/error.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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 (
|
||||||
|
// ErrWatchExit represents an exit from the Watch loop via chan closure.
|
||||||
|
ErrWatchExit = Error("watch exit")
|
||||||
|
|
||||||
|
// ErrSignalExit represents an exit from the Watch loop via exit signal.
|
||||||
|
ErrSignalExit = Error("signal exit")
|
||||||
|
)
|
||||||
83
engine/event/event.go
Normal file
83
engine/event/event.go
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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 event provides some primitives that are used for message passing.
|
||||||
|
package event
|
||||||
|
|
||||||
|
//go:generate stringer -type=Kind -output=kind_stringer.go
|
||||||
|
|
||||||
|
// Kind represents the type of event being passed.
|
||||||
|
type Kind int
|
||||||
|
|
||||||
|
// The different event kinds are used in different contexts.
|
||||||
|
const (
|
||||||
|
KindNil Kind = iota
|
||||||
|
KindStart
|
||||||
|
KindPause
|
||||||
|
KindPoke
|
||||||
|
KindExit
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pre-built messages so they can be used directly without having to use NewMsg.
|
||||||
|
// These are useful when we don't want a response via ACK().
|
||||||
|
var (
|
||||||
|
Start = &Msg{Kind: KindStart}
|
||||||
|
Pause = &Msg{Kind: KindPause} // probably unused b/c we want a resp
|
||||||
|
Poke = &Msg{Kind: KindPoke}
|
||||||
|
Exit = &Msg{Kind: KindExit}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Msg is an event primitive that represents a kind of event, and optionally a
|
||||||
|
// request for an ACK.
|
||||||
|
type Msg struct {
|
||||||
|
Kind Kind
|
||||||
|
|
||||||
|
resp chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsg builds a new message struct. It will want an ACK. If you don't want an
|
||||||
|
// ACK then use the pre-built messages in the package variable globals.
|
||||||
|
func NewMsg(kind Kind) *Msg {
|
||||||
|
return &Msg{
|
||||||
|
Kind: kind,
|
||||||
|
resp: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanACK determines if an ACK is possible for this message. It does not say
|
||||||
|
// whether one has already been sent or not.
|
||||||
|
func (obj *Msg) CanACK() bool {
|
||||||
|
return obj.resp != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ACK acknowledges the event. It must not be called more than once for the same
|
||||||
|
// event. It unblocks the past and future calls of Wait for this event.
|
||||||
|
func (obj *Msg) ACK() {
|
||||||
|
close(obj.resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait on ACK for this event. It doesn't matter if this runs before or after
|
||||||
|
// the ACK. It will unblock either way.
|
||||||
|
// TODO: consider adding a context if it's ever useful.
|
||||||
|
func (obj *Msg) Wait() error {
|
||||||
|
select {
|
||||||
|
//case <-ctx.Done():
|
||||||
|
// return ctx.Err()
|
||||||
|
case <-obj.resp:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
61
engine/fs.go
Normal file
61
engine/fs.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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)
|
||||||
|
}
|
||||||
478
engine/graph/actions.go
Normal file
478
engine/graph/actions.go
Normal file
@@ -0,0 +1,478 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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/engine/event"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
|
||||||
|
//multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
"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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine Guarantee: Do not allow CheckApply to run while we are paused.
|
||||||
|
// This makes the resource able to know that synchronous channel sending
|
||||||
|
// to the main loop select in Watch from within CheckApply, will succeed
|
||||||
|
// without blocking because the resource went into a paused state. If we
|
||||||
|
// are using the Poll metaparam, then Watch will (of course) not be run.
|
||||||
|
// FIXME: should this lock be here, or wrapped right around CheckApply ?
|
||||||
|
obj.state[vertex].eventsLock.Lock() // this lock is taken within Event()
|
||||||
|
defer obj.state[vertex].eventsLock.Unlock()
|
||||||
|
|
||||||
|
// backpoke! (can be async)
|
||||||
|
if vs := obj.BadTimestamps(vertex); len(vs) > 0 {
|
||||||
|
// back poke in parallel (sync b/c of waitgroup)
|
||||||
|
for _, v := range obj.graph.IncomingGraphVertices(vertex) {
|
||||||
|
if !pgraph.VertexContains(v, vs) { // only poke what's needed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
go obj.state[v].Poke() // async
|
||||||
|
|
||||||
|
}
|
||||||
|
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.
|
||||||
|
func (obj *Engine) Worker(vertex pgraph.Vertex) error {
|
||||||
|
res, isRes := vertex.(engine.Res)
|
||||||
|
if !isRes {
|
||||||
|
return fmt.Errorf("vertex is not a resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
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].outputChan) // we close this on behalf of res
|
||||||
|
|
||||||
|
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 event, ok := <-obj.state[vertex].init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.state[vertex].init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
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.ErrWatchExit || err == engine.ErrSignalExit {
|
||||||
|
return // exited cleanly, we're done
|
||||||
|
}
|
||||||
|
// we've got an error...
|
||||||
|
delay = res.MetaParams().Delay
|
||||||
|
|
||||||
|
if retry < 0 { // infinite retries
|
||||||
|
obj.state[vertex].reset()
|
||||||
|
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)
|
||||||
|
obj.state[vertex].reset()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
//if retry == 0 { // optional
|
||||||
|
// err = errwrap.Wrapf(err, "permanent watch error")
|
||||||
|
//}
|
||||||
|
break // break out of this and send the error
|
||||||
|
}
|
||||||
|
// 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].outputChan <- errwrap.Wrapf(err, "watch failed"):
|
||||||
|
// send
|
||||||
|
case <-obj.state[vertex].exit.Signal():
|
||||||
|
// pass
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// bonus safety check
|
||||||
|
if res.MetaParams().Burst == 0 && !(res.MetaParams().Limit == rate.Inf) { // blocked
|
||||||
|
return fmt.Errorf("permanently limited (rate != Inf, burst = 0)")
|
||||||
|
}
|
||||||
|
var limiter = rate.NewLimiter(res.MetaParams().Limit, res.MetaParams().Burst)
|
||||||
|
// It is important that we shutdown the Watch loop if this exits.
|
||||||
|
// Example, if Process errors permanently, we should ask Watch to exit.
|
||||||
|
defer obj.state[vertex].Event(event.Exit) // signal an exit
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err, ok := <-obj.state[vertex].outputChan: // read from watch channel
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err // permanent failure
|
||||||
|
}
|
||||||
|
|
||||||
|
// safe to go run the process...
|
||||||
|
case <-obj.state[vertex].exit.Signal(): // TODO: is this needed?
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
r := limiter.ReserveN(now, 1) // one event
|
||||||
|
// r.OK() seems to always be true here!
|
||||||
|
d := r.DelayFrom(now)
|
||||||
|
if d > 0 { // delay
|
||||||
|
obj.state[vertex].init.Logf("limited (rate: %v/sec, burst: %d, next: %v)", res.MetaParams().Limit, res.MetaParams().Burst, d)
|
||||||
|
var count int
|
||||||
|
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].outputChan: // read from watch channel
|
||||||
|
if !ok {
|
||||||
|
// FIXME: is this logic correct?
|
||||||
|
if count == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// loop, because we have
|
||||||
|
// the previous event to
|
||||||
|
// run process on first!
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return e // permanent failure
|
||||||
|
}
|
||||||
|
count++ // count the events...
|
||||||
|
limiter.ReserveN(time.Now(), 1) // one event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
timer.Stop() // it's nice to cleanup
|
||||||
|
obj.state[vertex].init.Logf("rate limiting expired!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var retry = res.MetaParams().Retry // lookup the retry value
|
||||||
|
var delay uint64
|
||||||
|
Loop:
|
||||||
|
for { // retry loop
|
||||||
|
if delay > 0 {
|
||||||
|
var count int
|
||||||
|
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].outputChan: // read from watch channel
|
||||||
|
if !ok {
|
||||||
|
// FIXME: is this logic correct?
|
||||||
|
if count == 0 {
|
||||||
|
// last process error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// loop, because we have
|
||||||
|
// the previous event to
|
||||||
|
// run process on first!
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e != nil {
|
||||||
|
return e // permanent failure
|
||||||
|
}
|
||||||
|
count++ // count the events...
|
||||||
|
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 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 Loop
|
||||||
|
}
|
||||||
|
// 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")
|
||||||
|
//}
|
||||||
|
|
||||||
|
// If this exits, defer calls: obj.Event(event.Exit),
|
||||||
|
// which will cause the Watch loop to shutdown. Also,
|
||||||
|
// if the Watch loop shuts down, that will cause this
|
||||||
|
// Process loop to shut down. Also the graph sync can
|
||||||
|
// run an: obj.Event(event.Exit) which causes this to
|
||||||
|
// shutdown as well. Lastly, it is possible that more
|
||||||
|
// that one of these scenarios happens simultaneously.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//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-2018+ 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)
|
||||||
|
}
|
||||||
155
engine/graph/autoedge/autoedge.go
Normal file
155
engine/graph/autoedge/autoedge.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 = multierr.Append(err, e) // collect all errors
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if autoEdgeObj == nil {
|
||||||
|
logf("no auto edges were found for: %s", res)
|
||||||
|
continue // next vertex
|
||||||
|
}
|
||||||
|
autoEdgeObjMap[res] = autoEdgeObj // save for next loop
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the auto edges had errors")
|
||||||
|
}
|
||||||
|
|
||||||
|
// now that we're guaranteed error free, we can modify the graph safely
|
||||||
|
for _, res := range sorted { // stable sort order for determinism in logs
|
||||||
|
autoEdgeObj, exists := autoEdgeObjMap[res]
|
||||||
|
if !exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for { // while the autoEdgeObj has more uids to add...
|
||||||
|
uids := autoEdgeObj.Next() // get some!
|
||||||
|
if uids == nil {
|
||||||
|
logf("the auto edge list is empty for: %s", res)
|
||||||
|
break // inner loop
|
||||||
|
}
|
||||||
|
if debug {
|
||||||
|
logf("autoedge: UIDS:")
|
||||||
|
for i, u := range uids {
|
||||||
|
logf("autoedge: UID%d: %v", i, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match and add edges
|
||||||
|
result := addEdgesByMatchingUIDS(res, uids, graph, debug, logf)
|
||||||
|
|
||||||
|
// report back, and find out if we should continue
|
||||||
|
if !autoEdgeObj.Test(result) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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
|
||||||
|
}
|
||||||
141
engine/graph/autogroup.go
Normal file
141
engine/graph/autogroup.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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 *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
|
||||||
|
}
|
||||||
71
engine/graph/autogroup/autogroup.go
Normal file
71
engine/graph/autogroup/autogroup.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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("success for: %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!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
921
engine/graph/autogroup/autogroup_test.go
Normal file
921
engine/graph/autogroup/autogroup_test.go
Normal file
@@ -0,0 +1,921 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
127
engine/graph/autogroup/base.go
Normal file
127
engine/graph/autogroup/base.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 simple 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
|
||||||
|
}
|
||||||
73
engine/graph/autogroup/nonreachability.go
Normal file
73
engine/graph/autogroup/nonreachability.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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...
|
||||||
|
}
|
||||||
|
}
|
||||||
127
engine/graph/autogroup/util.go
Normal file
127
engine/graph/autogroup/util.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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 // TODO: ineffassign?
|
||||||
|
//*v1 = *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
348
engine/graph/engine.go
Normal file
348
engine/graph/engine.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/converger"
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/event"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
"github.com/purpleidea/mgmt/util/semaphore"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine encapsulates a generic graph and manages its operations.
|
||||||
|
type Engine struct {
|
||||||
|
Program string
|
||||||
|
Hostname string
|
||||||
|
World engine.World
|
||||||
|
|
||||||
|
// Prefix is a unique directory prefix which can be used. It should be
|
||||||
|
// created if needed.
|
||||||
|
Prefix string
|
||||||
|
Converger converger.Converger
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
slock *sync.Mutex // semaphore lock
|
||||||
|
semas map[string]*semaphore.Semaphore
|
||||||
|
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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.slock = &sync.Mutex{}
|
||||||
|
obj.semas = make(map[string]*semaphore.Semaphore)
|
||||||
|
|
||||||
|
obj.wg = &sync.WaitGroup{}
|
||||||
|
|
||||||
|
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 ?
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: is res.Name() sufficiently unique to use as a UID here?
|
||||||
|
pathUID := fmt.Sprintf("%s-%s", res.Kind(), res.Name())
|
||||||
|
statePrefix := fmt.Sprintf("%s/", path.Join(obj.Prefix, "state", 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, // TODO: what happens if we swap the graph?
|
||||||
|
Vertex: vertex,
|
||||||
|
|
||||||
|
Program: obj.Program,
|
||||||
|
Hostname: obj.Hostname,
|
||||||
|
|
||||||
|
World: obj.World,
|
||||||
|
Prefix: statePrefix,
|
||||||
|
//Converger: obj.Converger,
|
||||||
|
|
||||||
|
Debug: obj.Debug,
|
||||||
|
Logf: func(format string, v ...interface{}) {
|
||||||
|
obj.Logf(res.String()+": "+format, v...)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := obj.state[vertex].Init(); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the Res did not Init")
|
||||||
|
}
|
||||||
|
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!
|
||||||
|
obj.state[vertex].Event(event.Exit) // signal an exit
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, engine.VertexCmpFn, vertexAddFn, vertexRemoveFn, engine.EdgeCmpFn); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error running graph sync")
|
||||||
|
}
|
||||||
|
// we run these afterwards, so that the state structs (that might get
|
||||||
|
// referenced) aren't 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.
|
||||||
|
|
||||||
|
// TODO: 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start runs the currently active graph. It also un-pauses the graph if it was
|
||||||
|
// paused.
|
||||||
|
func (obj *Engine) Start() error {
|
||||||
|
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 {
|
||||||
|
state := obj.state[vertex]
|
||||||
|
state.starter = (indegree[vertex] == 0)
|
||||||
|
var unpause = true // assume true
|
||||||
|
|
||||||
|
if !state.working { // if not running...
|
||||||
|
state.working = true
|
||||||
|
unpause = false // doesn't need unpausing if starting
|
||||||
|
obj.wg.Add(1)
|
||||||
|
obj.waits[vertex].Add(1)
|
||||||
|
go func(v pgraph.Vertex) {
|
||||||
|
defer obj.wg.Done()
|
||||||
|
defer obj.waits[vertex].Done()
|
||||||
|
defer func() {
|
||||||
|
obj.state[v].working = false
|
||||||
|
}()
|
||||||
|
|
||||||
|
obj.Logf("Worker(%s)", v)
|
||||||
|
// contains the Watch and CheckApply loops
|
||||||
|
err := obj.Worker(v)
|
||||||
|
obj.Logf("Worker(%s): Exited(%+v)", v, err)
|
||||||
|
}(vertex)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-state.started:
|
||||||
|
case <-state.stopped: // we failed on Watch start
|
||||||
|
}
|
||||||
|
|
||||||
|
if unpause { // unpause (if needed)
|
||||||
|
obj.state[vertex].Event(event.Start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we wait for everyone to start before exiting!
|
||||||
|
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.
|
||||||
|
func (obj *Engine) SetFastPause() {
|
||||||
|
obj.fastPause = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pause the active, running graph. At the moment this cannot error.
|
||||||
|
func (obj *Engine) Pause(fastPause bool) {
|
||||||
|
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.
|
||||||
|
obj.state[vertex].Event(event.Pause)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we are now completely paused...
|
||||||
|
obj.fastPause = false // reset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close triggers a shutdown. Engine must be already paused before this is run.
|
||||||
|
func (obj *Engine) Close() error {
|
||||||
|
var reterr error
|
||||||
|
|
||||||
|
emptyGraph, err := pgraph.NewGraph("empty")
|
||||||
|
if err != nil {
|
||||||
|
reterr = multierr.Append(reterr, err) // list of errors
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = multierr.Append(reterr, err)
|
||||||
|
}
|
||||||
|
// the commit will cause the graph sync to shut things down cleverly...
|
||||||
|
if err := obj.Commit(); err != nil {
|
||||||
|
reterr = multierr.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
|
||||||
|
}
|
||||||
59
engine/graph/refresh.go
Normal file
59
engine/graph/refresh.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
engine/graph/semaphore.go
Normal file
87
engine/graph/semaphore.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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/semaphore"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SemaSep is the trailing separator to split the semaphore id from the size.
|
||||||
|
const SemaSep = ":"
|
||||||
|
|
||||||
|
// semaLock acquires the list of semaphores in the graph.
|
||||||
|
func (obj *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()
|
||||||
|
|
||||||
|
if err := sema.P(1); err != nil { // lock!
|
||||||
|
reterr = multierr.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))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sema.V(1); err != nil { // unlock!
|
||||||
|
reterr = multierr.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-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSemaSize(t *testing.T) {
|
||||||
|
pairs := map[string]int{
|
||||||
|
"id:42": 42,
|
||||||
|
":13": 13,
|
||||||
|
"some_id": 1,
|
||||||
|
}
|
||||||
|
for id, size := range pairs {
|
||||||
|
if i := SemaSize(id); i != size {
|
||||||
|
t.Errorf("sema id `%s`, expected: `%d`, got: `%d`", id, size, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
118
engine/graph/sendrecv.go
Normal file
118
engine/graph/sendrecv.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
// send
|
||||||
|
obj1 := reflect.Indirect(reflect.ValueOf(st))
|
||||||
|
type1 := obj1.Type()
|
||||||
|
value1 := obj1.FieldByName(v.Key)
|
||||||
|
kind1 := value1.Kind()
|
||||||
|
|
||||||
|
// recv
|
||||||
|
obj2 := reflect.Indirect(reflect.ValueOf(res)) // pass in full struct
|
||||||
|
type2 := obj2.Type()
|
||||||
|
value2 := obj2.FieldByName(k)
|
||||||
|
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 = multierr.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 = multierr.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 = multierr.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 = multierr.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
|
||||||
|
}
|
||||||
453
engine/graph/state.go
Normal file
453
engine/graph/state.go
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package graph
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/converger"
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/event"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State stores some state about the resource it is mapped to.
|
||||||
|
type State struct {
|
||||||
|
// Graph is a pointer to the graph that this vertex is part of.
|
||||||
|
//Graph pgraph.Graph
|
||||||
|
|
||||||
|
// Vertex is the pointer in the graph that this state corresponds to. It
|
||||||
|
// can be converted to a `Res` if necessary.
|
||||||
|
// TODO: should this be passed in on Init instead?
|
||||||
|
Vertex pgraph.Vertex
|
||||||
|
|
||||||
|
Program string
|
||||||
|
Hostname string
|
||||||
|
World engine.World
|
||||||
|
|
||||||
|
// Prefix is a unique directory prefix which can be used. It should be
|
||||||
|
// created if needed.
|
||||||
|
Prefix string
|
||||||
|
|
||||||
|
//Converger converger.Converger
|
||||||
|
|
||||||
|
// 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 ?
|
||||||
|
|
||||||
|
// events is a channel of incoming events which is read by the Watch
|
||||||
|
// loop for that resource. It receives events like pause, start, and
|
||||||
|
// poke. The channel shuts down to signal for Watch to exit.
|
||||||
|
eventsChan chan *event.Msg // incoming to resource
|
||||||
|
eventsLock *sync.Mutex // lock around sending and closing of events channel
|
||||||
|
eventsDone bool // is channel closed?
|
||||||
|
|
||||||
|
// outputChan 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.
|
||||||
|
outputChan chan error // outgoing from resource
|
||||||
|
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
exit *util.EasyExit
|
||||||
|
|
||||||
|
started chan struct{} // closes when it's started
|
||||||
|
stopped chan struct{} // closes when it's stopped
|
||||||
|
|
||||||
|
starter bool // do we have an indegree of 0 ?
|
||||||
|
working bool // is the Main() loop running ?
|
||||||
|
|
||||||
|
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 {
|
||||||
|
obj.eventsChan = make(chan *event.Msg)
|
||||||
|
obj.eventsLock = &sync.Mutex{}
|
||||||
|
|
||||||
|
obj.outputChan = make(chan error)
|
||||||
|
|
||||||
|
obj.wg = &sync.WaitGroup{}
|
||||||
|
obj.exit = util.NewEasyExit()
|
||||||
|
|
||||||
|
obj.started = make(chan struct{})
|
||||||
|
obj.stopped = make(chan struct{})
|
||||||
|
|
||||||
|
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.cuid = obj.Converger.Register() // gets registered in Worker()
|
||||||
|
//obj.tuid = obj.Converger.Register() // gets registered in Worker()
|
||||||
|
|
||||||
|
obj.init = &engine.Init{
|
||||||
|
Program: obj.Program,
|
||||||
|
Hostname: obj.Hostname,
|
||||||
|
|
||||||
|
// Watch:
|
||||||
|
Running: func() error {
|
||||||
|
obj.tuid.StopTimer()
|
||||||
|
close(obj.started) // this is reset in the reset func
|
||||||
|
obj.isStateOK = false // assume we're initially dirty
|
||||||
|
// optimization: skip the initial send if not a starter
|
||||||
|
// because we'll get poked from a starter soon anyways!
|
||||||
|
if !obj.starter {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return obj.event()
|
||||||
|
},
|
||||||
|
Event: obj.event,
|
||||||
|
Events: obj.eventsChan,
|
||||||
|
Read: obj.read,
|
||||||
|
Dirty: func() { // TODO: should we rename this SetDirty?
|
||||||
|
obj.tuid.StopTimer()
|
||||||
|
obj.isStateOK = false
|
||||||
|
},
|
||||||
|
|
||||||
|
// CheckApply:
|
||||||
|
Refresh: func() bool {
|
||||||
|
res, ok := obj.Vertex.(engine.RefreshableRes)
|
||||||
|
if !ok {
|
||||||
|
panic("res does not support the Refreshable trait")
|
||||||
|
}
|
||||||
|
return res.Refresh()
|
||||||
|
},
|
||||||
|
Send: func(st interface{}) error {
|
||||||
|
res, ok := obj.Vertex.(engine.SendableRes)
|
||||||
|
if !ok {
|
||||||
|
panic("res does not support the Sendable trait")
|
||||||
|
}
|
||||||
|
// XXX: type check this
|
||||||
|
//expected := res.Sends()
|
||||||
|
//if err := XXX_TYPE_CHECK(expected, st); err != nil {
|
||||||
|
// return err
|
||||||
|
//}
|
||||||
|
|
||||||
|
return res.Send(st) // send the struct
|
||||||
|
},
|
||||||
|
Recv: func() map[string]*engine.Send { // TODO: change this API?
|
||||||
|
res, ok := obj.Vertex.(engine.RecvableRes)
|
||||||
|
if !ok {
|
||||||
|
panic("res does not support the Recvable trait")
|
||||||
|
}
|
||||||
|
return res.Recv()
|
||||||
|
},
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
err := res.Close()
|
||||||
|
if obj.Debug {
|
||||||
|
obj.Logf("Close(%s): Return(%+v)", res, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset is run to reset the state so that Watch can run a second time. Thus is
|
||||||
|
// needed for the Watch retry in particular.
|
||||||
|
func (obj *State) reset() {
|
||||||
|
obj.started = make(chan struct{})
|
||||||
|
obj.stopped = make(chan struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poke sends a nil message on the outputChan. This channel is used by the
|
||||||
|
// resource to signal a possible change. This will cause the Process loop to
|
||||||
|
// run if it can.
|
||||||
|
func (obj *State) Poke() {
|
||||||
|
// add a wait group on the vertex we're poking!
|
||||||
|
obj.wg.Add(1)
|
||||||
|
defer obj.wg.Done()
|
||||||
|
|
||||||
|
// now that we've added to the wait group, obj.outputChan won't close...
|
||||||
|
// so see if there's an exit signal before we release the wait group!
|
||||||
|
// XXX: i don't think this is necessarily happening, but maybe it is?
|
||||||
|
// XXX: re-write some of the engine to ensure that: "the sender closes"!
|
||||||
|
select {
|
||||||
|
case <-obj.exit.Signal():
|
||||||
|
return // skip sending the poke b/c we're closing
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case obj.outputChan <- nil:
|
||||||
|
|
||||||
|
case <-obj.exit.Signal():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event sends a Pause or Start event to the resource. It can also be used to
|
||||||
|
// send Poke events, but it's much more efficient to send them directly instead
|
||||||
|
// of passing them through the resource.
|
||||||
|
func (obj *State) Event(msg *event.Msg) {
|
||||||
|
// TODO: should these happen after the lock?
|
||||||
|
obj.wg.Add(1)
|
||||||
|
defer obj.wg.Done()
|
||||||
|
|
||||||
|
obj.eventsLock.Lock()
|
||||||
|
defer obj.eventsLock.Unlock()
|
||||||
|
|
||||||
|
if obj.eventsDone { // closing, skip events...
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Kind == event.KindExit { // set this so future events don't deadlock
|
||||||
|
obj.Logf("exit event...")
|
||||||
|
obj.eventsDone = true
|
||||||
|
close(obj.eventsChan) // causes resource Watch loop to close
|
||||||
|
obj.exit.Done(nil) // trigger exit signal to unblock some cases
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case obj.eventsChan <- msg:
|
||||||
|
|
||||||
|
case <-obj.exit.Signal():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read is a helper function used inside the main select statement of resources.
|
||||||
|
// If it returns an error, then this is a signal for the resource to exit.
|
||||||
|
func (obj *State) read(msg *event.Msg) error {
|
||||||
|
switch msg.Kind {
|
||||||
|
case event.KindPoke:
|
||||||
|
return obj.event() // a poke needs to cause an event...
|
||||||
|
case event.KindStart:
|
||||||
|
return fmt.Errorf("unexpected start")
|
||||||
|
case event.KindPause:
|
||||||
|
// pass
|
||||||
|
case event.KindExit:
|
||||||
|
return engine.ErrSignalExit
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled event: %+v", msg.Kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're paused now
|
||||||
|
select {
|
||||||
|
case msg, ok := <-obj.eventsChan:
|
||||||
|
if !ok {
|
||||||
|
return engine.ErrWatchExit
|
||||||
|
}
|
||||||
|
switch msg.Kind {
|
||||||
|
case event.KindPoke:
|
||||||
|
return fmt.Errorf("unexpected poke")
|
||||||
|
case event.KindPause:
|
||||||
|
return fmt.Errorf("unexpected pause")
|
||||||
|
case event.KindStart:
|
||||||
|
// resumed
|
||||||
|
return nil
|
||||||
|
case event.KindExit:
|
||||||
|
return engine.ErrSignalExit
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled event: %+v", msg.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// event is a helper function to send an event from the resource Watch loop. It
|
||||||
|
// can be used for the initial `running` event, or any regular event. If it
|
||||||
|
// returns an error, then the Watch loop must return this error and shutdown.
|
||||||
|
func (obj *State) event() error {
|
||||||
|
// loop until we sent on obj.outputChan or exit with error
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// send "activity" event
|
||||||
|
case obj.outputChan <- nil:
|
||||||
|
return nil // sent event!
|
||||||
|
|
||||||
|
// make sure to keep handling incoming
|
||||||
|
case msg, ok := <-obj.eventsChan:
|
||||||
|
if !ok {
|
||||||
|
return engine.ErrWatchExit
|
||||||
|
}
|
||||||
|
switch msg.Kind {
|
||||||
|
case event.KindPoke:
|
||||||
|
// we're trying to send an event, so swallow the
|
||||||
|
// poke: it's what we wanted to have happen here
|
||||||
|
continue
|
||||||
|
case event.KindStart:
|
||||||
|
return fmt.Errorf("unexpected start")
|
||||||
|
case event.KindPause:
|
||||||
|
// pass
|
||||||
|
case event.KindExit:
|
||||||
|
return engine.ErrSignalExit
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled event: %+v", msg.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're paused now
|
||||||
|
select {
|
||||||
|
case msg, ok := <-obj.eventsChan:
|
||||||
|
if !ok {
|
||||||
|
return engine.ErrWatchExit
|
||||||
|
}
|
||||||
|
switch msg.Kind {
|
||||||
|
case event.KindPoke:
|
||||||
|
return fmt.Errorf("unexpected poke")
|
||||||
|
case event.KindPause:
|
||||||
|
return fmt.Errorf("unexpected pause")
|
||||||
|
case event.KindStart:
|
||||||
|
// resumed
|
||||||
|
case event.KindExit:
|
||||||
|
return engine.ErrSignalExit
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unhandled event: %+v", msg.Kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C: // received the timer event
|
||||||
|
obj.init.Logf("polling...")
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
173
engine/metaparams.go
Normal file
173
engine/metaparams.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
"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{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMetaCmp1(t *testing.T) {
|
||||||
|
m1 := &MetaParams{
|
||||||
|
Noop: true,
|
||||||
|
}
|
||||||
|
m2 := &MetaParams{
|
||||||
|
Noop: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should we allow this? Maybe only with the future Mutate API?
|
||||||
|
//if err := m2.Cmp(m1); err != nil { // going from noop(false) -> noop(true) is okay!
|
||||||
|
// t.Errorf("the two resources do not match")
|
||||||
|
//}
|
||||||
|
|
||||||
|
if m1.Cmp(m2) == nil { // going from noop(true) -> noop(false) is not okay!
|
||||||
|
t.Errorf("the two resources should not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
32
engine/refresh.go
Normal file
32
engine/refresh.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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)
|
||||||
|
}
|
||||||
308
engine/resources.go
Normal file
308
engine/resources.go
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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/engine/event"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: should each resource be a sub-package?
|
||||||
|
var registeredResources = map[string]func() Res{}
|
||||||
|
|
||||||
|
// RegisterResource registers a new resource by providing a constructor
|
||||||
|
// function that returns a resource object ready to be unmarshalled from YAML.
|
||||||
|
func RegisterResource(kind string, fn func() Res) {
|
||||||
|
f := fn()
|
||||||
|
if kind == "" {
|
||||||
|
panic("can't register a resource with an empty kind")
|
||||||
|
}
|
||||||
|
if _, ok := registeredResources[kind]; ok {
|
||||||
|
panic(fmt.Sprintf("a resource kind of %s is already registered", kind))
|
||||||
|
}
|
||||||
|
gob.Register(f)
|
||||||
|
registeredResources[kind] = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisteredResourcesNames returns the kind of the registered resources.
|
||||||
|
func RegisteredResourcesNames() []string {
|
||||||
|
kinds := []string{}
|
||||||
|
for k := range registeredResources {
|
||||||
|
kinds = append(kinds, k)
|
||||||
|
}
|
||||||
|
return kinds
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewResource returns an empty resource object from a registered kind. It
|
||||||
|
// errors if the resource kind doesn't exist.
|
||||||
|
func NewResource(kind string) (Res, error) {
|
||||||
|
fn, ok := registeredResources[kind]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no resource kind `%s` available", kind)
|
||||||
|
}
|
||||||
|
res := fn().Default()
|
||||||
|
res.SetKind(kind)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNamedResource returns an empty resource object from a registered kind. It
|
||||||
|
// also sets the name. It is a wrapper around NewResource. It also errors if the
|
||||||
|
// name is empty.
|
||||||
|
func NewNamedResource(kind, name string) (Res, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("resource name is empty")
|
||||||
|
}
|
||||||
|
res, err := NewResource(kind)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.SetName(name)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init is the structure of values and references which is passed into all
|
||||||
|
// resources on initialization. None of these are available in Validate, or
|
||||||
|
// before Init runs.
|
||||||
|
type Init struct {
|
||||||
|
// Program is the name of the program.
|
||||||
|
Program string
|
||||||
|
|
||||||
|
// Hostname is the uuid for the host.
|
||||||
|
Hostname string
|
||||||
|
|
||||||
|
// Called from within Watch:
|
||||||
|
|
||||||
|
// Running must be called after your watches are all started and ready.
|
||||||
|
Running func() error
|
||||||
|
|
||||||
|
// Event sends an event notifying the engine of a possible state change.
|
||||||
|
Event func() error
|
||||||
|
|
||||||
|
// Events returns a channel that we must watch for messages from the
|
||||||
|
// engine. When it closes, this is a signal to shutdown.
|
||||||
|
Events chan *event.Msg
|
||||||
|
|
||||||
|
// Read processes messages that come in from the Events channel. It is a
|
||||||
|
// helper method that knows how to handle the pause mechanism correctly.
|
||||||
|
Read func(*event.Msg) error
|
||||||
|
|
||||||
|
// Dirty marks the resource state as dirty. This signals to the engine
|
||||||
|
// that CheckApply will have some work to do in order to converge it.
|
||||||
|
Dirty func()
|
||||||
|
|
||||||
|
// 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:
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
333
engine/resources/augeas.go
Normal file
333
engine/resources/augeas.go
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !noaugeas
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/recwatch"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
// FIXME: we vendor go/augeas because master requires augeas 1.6.0
|
||||||
|
// and libaugeas-dev-1.6.0 is not yet available in a PPA.
|
||||||
|
"honnef.co/go/augeas"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
|
// 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()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
1441
engine/resources/aws_ec2.go
Normal file
1441
engine/resources/aws_ec2.go
Normal file
File diff suppressed because it is too large
Load Diff
568
engine/resources/cron.go
Normal file
568
engine/resources/cron.go
Normal file
@@ -0,0 +1,568 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
sdbus "github.com/coreos/go-systemd/dbus"
|
||||||
|
"github.com/coreos/go-systemd/unit"
|
||||||
|
systemdUtil "github.com/coreos/go-systemd/util"
|
||||||
|
"github.com/godbus/dbus"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-dbusChan:
|
||||||
|
// process dbus events
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("%+v", event)
|
||||||
|
}
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
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
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (checkOK bool, err error) {
|
||||||
|
ok := 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 {
|
||||||
|
ok = 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 {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
return ok, 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) (checkOK bool, err error) {
|
||||||
|
var conn *sdbus.Conn
|
||||||
|
var godbusConn *dbus.Conn
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
441
engine/resources/docker_container.go
Normal file
441
engine/resources/docker_container.go
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !nodocker
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
"github.com/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"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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, 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{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.NewClient(client.DefaultDockerHost, obj.APIVersion, nil, nil)
|
||||||
|
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{})
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
case err, ok := <-errChan:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Docker resource.
|
||||||
|
func (obj *DockerContainerRes) CheckApply(apply bool) (checkOK bool, err 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, 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.Name() != res.Name() {
|
||||||
|
return fmt.Errorf("names differ")
|
||||||
|
}
|
||||||
|
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "cmd differs")
|
||||||
|
}
|
||||||
|
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "env differs")
|
||||||
|
}
|
||||||
|
if len(obj.Ports) != len(res.Ports) {
|
||||||
|
return fmt.Errorf("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("ports differ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.APIVersion != res.APIVersion {
|
||||||
|
return fmt.Errorf("apiversions differ")
|
||||||
|
}
|
||||||
|
if obj.Force != res.Force {
|
||||||
|
return fmt.Errorf("forces differ")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DockerUID is the UID struct for DockerContainerRes.
|
||||||
|
type DockerUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *DockerContainerRes) UIDs() []engine.ResUID {
|
||||||
|
x := &DockerUID{
|
||||||
|
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
|
||||||
|
}
|
||||||
201
engine/resources/docker_container_test.go
Normal file
201
engine/resources/docker_container_test.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !nodocker
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
var res *DockerContainerRes
|
||||||
|
|
||||||
|
var id string
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
var setupCode, testCode, cleanupCode int
|
||||||
|
|
||||||
|
if err := setup(); err != nil {
|
||||||
|
log.Printf("error during setup: %s", err)
|
||||||
|
setupCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if setupCode == 0 {
|
||||||
|
testCode = m.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cleanup(); err != nil {
|
||||||
|
log.Printf("error during cleanup: %s", err)
|
||||||
|
cleanupCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Exit(setupCode + testCode + cleanupCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_containerStart(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := res.containerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
|
||||||
|
t.Errorf("containerStart() error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := res.client.ContainerList(
|
||||||
|
ctx,
|
||||||
|
types.ContainerListOptions{
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.KeyValuePair{Key: "id", Value: id},
|
||||||
|
filters.KeyValuePair{Key: "status", Value: "running"},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error listing containers: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(l) != 1 {
|
||||||
|
t.Errorf("failed to start container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_containerStop(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := res.containerStop(ctx, id, nil); err != nil {
|
||||||
|
t.Errorf("containerStop() error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := res.client.ContainerList(
|
||||||
|
ctx,
|
||||||
|
types.ContainerListOptions{
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.KeyValuePair{Key: "id", Value: id},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error listing containers: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(l) != 0 {
|
||||||
|
t.Errorf("failed to stop container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_containerRemove(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := res.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
|
||||||
|
t.Errorf("containerRemove() error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := res.client.ContainerList(
|
||||||
|
ctx,
|
||||||
|
types.ContainerListOptions{
|
||||||
|
All: true,
|
||||||
|
Filters: filters.NewArgs(
|
||||||
|
filters.KeyValuePair{Key: "id", Value: id},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error listing containers: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(l) != 0 {
|
||||||
|
t.Errorf("failed to remove container")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
res = &DockerContainerRes{}
|
||||||
|
res.Init(res.init)
|
||||||
|
|
||||||
|
p, err := res.client.ImagePull(ctx, "alpine", types.ImagePullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error pulling image: %s", err)
|
||||||
|
}
|
||||||
|
if _, err := ioutil.ReadAll(p); err != nil {
|
||||||
|
return fmt.Errorf("error reading image pull result: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := res.client.ContainerCreate(
|
||||||
|
ctx,
|
||||||
|
&container.Config{
|
||||||
|
Image: "alpine",
|
||||||
|
Cmd: []string{"sleep", "100"},
|
||||||
|
},
|
||||||
|
&container.HostConfig{},
|
||||||
|
nil,
|
||||||
|
"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
|
||||||
|
}
|
||||||
621
engine/resources/exec.go
Normal file
621
engine/resources/exec.go
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
Cmd string `yaml:"cmd"` // the command to run
|
||||||
|
Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd
|
||||||
|
Timeout int `yaml:"timeout"` // the cmd timeout in seconds
|
||||||
|
WatchCmd string `yaml:"watchcmd"` // the watch command to run
|
||||||
|
WatchShell string `yaml:"watchshell"` // the (optional) shell to use to run the watch cmd
|
||||||
|
IfCmd string `yaml:"ifcmd"` // the if command to run
|
||||||
|
IfShell string `yaml:"ifshell"` // the (optional) shell to use to run the if cmd
|
||||||
|
User string `yaml:"user"` // the (optional) user to use to execute the command
|
||||||
|
Group string `yaml:"group"` // the (optional) group to use to execute the command
|
||||||
|
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!
|
||||||
|
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *ExecRes) Default() engine.Res {
|
||||||
|
return &ExecRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *ExecRes) Validate() error {
|
||||||
|
if obj.Cmd == "" { // this is the only thing that is really required
|
||||||
|
return fmt.Errorf("command can't be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 errwrap.Errorf("running as root is required if you want to use exec with a different user/group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.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 *bufioOutput)
|
||||||
|
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}
|
||||||
|
}
|
||||||
|
cmd := exec.Command(cmdName, cmdArgs...)
|
||||||
|
//cmd.Dir = "" // look for program in pwd ?
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdReader, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error creating StdoutPipe for Cmd")
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(cmdReader)
|
||||||
|
|
||||||
|
defer cmd.Wait() // wait for the command to exit before return!
|
||||||
|
defer func() {
|
||||||
|
// FIXME: without wrapping this in this func it panic's
|
||||||
|
// when running certain graphs... why?
|
||||||
|
cmd.Process.Kill() // shutdown the Watch command on exit
|
||||||
|
}()
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error starting Cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
ioChan = obj.bufioChanScanner(scanner)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
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?
|
||||||
|
return errwrap.Wrapf(err, "unknown error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// each time we get a line of output, we loop!
|
||||||
|
obj.init.Logf("watch output: %s", data.text)
|
||||||
|
if data.text != "" {
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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...)
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
// TODO: check exit value
|
||||||
|
return true, nil // don't run
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Cmd)
|
||||||
|
cmdName = split[0]
|
||||||
|
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||||
|
//cmdName = path.Join(d, cmdName)
|
||||||
|
cmdArgs = split[1:]
|
||||||
|
} else {
|
||||||
|
cmdName = obj.Shell // usually bash, or sh
|
||||||
|
cmdArgs = []string{"-c", obj.Cmd}
|
||||||
|
}
|
||||||
|
cmd := exec.Command(cmdName, cmdArgs...)
|
||||||
|
//cmd.Dir = "" // look for program in pwd ?
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeout := obj.Timeout
|
||||||
|
if timeout == 0 { // zero timeout means no timer, so disable it
|
||||||
|
timeout = -1
|
||||||
|
}
|
||||||
|
done := make(chan error)
|
||||||
|
go func() { done <- cmd.Wait() }()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case e := <-done:
|
||||||
|
err = e // store
|
||||||
|
|
||||||
|
case <-util.TimeAfterOrBlock(timeout):
|
||||||
|
cmd.Process.Kill() // TODO: check error?
|
||||||
|
return false, fmt.Errorf("timeout for cmd")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
return false, fmt.Errorf("cmd error, exit status: %d", wStatus.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(out.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *ExecRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare ExecRes to others of the same resource kind
|
||||||
|
res, ok := r.(*ExecRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Cmd != res.Cmd {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Shell != res.Shell {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Timeout != res.Timeout {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.WatchCmd != res.WatchCmd {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.WatchShell != res.WatchShell {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.IfCmd != res.IfCmd {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.IfShell != res.IfShell {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.User != res.User {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Group != res.Group {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next automatic edge.
|
||||||
|
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
|
||||||
|
return obj.edges
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test gets results of the earlier Next() call, & returns if we should continue!
|
||||||
|
func (obj *ExecResAutoEdges) Test(input []bool) bool {
|
||||||
|
return false // never keep going
|
||||||
|
// TODO: we could return false if we find as many edges as the number of different path's in cmdFiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
|
||||||
|
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
|
||||||
|
var data []engine.ResUID
|
||||||
|
for _, x := range obj.cmdFiles() {
|
||||||
|
var reversed = true
|
||||||
|
data = append(data, &PkgFileUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.Name(),
|
||||||
|
Kind: obj.Kind(),
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
path: x, // what matters
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &ExecResAutoEdges{
|
||||||
|
edges: data,
|
||||||
|
}, 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.Cmd,
|
||||||
|
IfCmd: obj.IfCmd,
|
||||||
|
// TODO: add more params here
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Cmd); 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufioOutput is the output struct of the bufioChanScanner channel output.
|
||||||
|
type bufioOutput struct {
|
||||||
|
text string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufioChanScanner wraps the scanner output in a channel.
|
||||||
|
func (obj *ExecRes) bufioChanScanner(scanner *bufio.Scanner) chan *bufioOutput {
|
||||||
|
ch := make(chan *bufioOutput)
|
||||||
|
obj.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer obj.wg.Done()
|
||||||
|
defer close(ch)
|
||||||
|
for scanner.Scan() {
|
||||||
|
ch <- &bufioOutput{text: scanner.Text()} // blocks here ?
|
||||||
|
}
|
||||||
|
// on EOF, scanner.Err() will be nil
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
ch <- &bufioOutput{err: err} // send any misc errors we encounter
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
190
engine/resources/exec_test.go
Normal file
190
engine/resources/exec_test.go
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeInit(t *testing.T) *engine.Init {
|
||||||
|
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||||
|
logf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf("test: "+format, v...)
|
||||||
|
}
|
||||||
|
return &engine.Init{
|
||||||
|
Running: func() error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Debug: debug,
|
||||||
|
Logf: logf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(fakeInit(t)); 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", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stdout == nil {
|
||||||
|
t.Errorf("stdout is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(fakeInit(t)); 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", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stderr == nil {
|
||||||
|
t.Errorf("stderr is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if err := r1.Init(fakeInit(t)); 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", r1.Output)
|
||||||
|
if r1.Output != nil {
|
||||||
|
t.Logf("output is: %v", *r1.Output)
|
||||||
|
}
|
||||||
|
t.Logf("stdout is: %v", r1.Stdout)
|
||||||
|
if r1.Stdout != nil {
|
||||||
|
t.Logf("stdout is: %v", *r1.Stdout)
|
||||||
|
}
|
||||||
|
t.Logf("stderr is: %v", r1.Stderr)
|
||||||
|
if r1.Stderr != nil {
|
||||||
|
t.Logf("stderr is: %v", *r1.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.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 := *r1.Output; out != "hello world\ngoodbye world\n" && out != "goodbye world\nhello world\n" {
|
||||||
|
t.Errorf("got wrong output(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stdout == nil {
|
||||||
|
t.Errorf("stdout is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stdout; out != "hello world\n" {
|
||||||
|
t.Errorf("got wrong stdout(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Stderr == nil {
|
||||||
|
t.Errorf("stderr is nil")
|
||||||
|
} else {
|
||||||
|
if out := *r1.Stderr; out != "goodbye world\n" {
|
||||||
|
t.Errorf("got wrong stderr(%d): %s", len(out), out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
164
engine/resources/file_test.go
Normal file
164
engine/resources/file_test.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/graph/autoedge"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
|
"github.com/purpleidea/mgmt/pgraph"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileAutoEdge1(t *testing.T) {
|
||||||
|
|
||||||
|
g, err := pgraph.NewGraph("TestGraph")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating graph: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r1 := &FileRes{
|
||||||
|
Path: "/tmp/a/b/", // some dir
|
||||||
|
}
|
||||||
|
r2 := &FileRes{
|
||||||
|
Path: "/tmp/a/", // some parent dir
|
||||||
|
}
|
||||||
|
r3 := &FileRes{
|
||||||
|
Path: "/tmp/a/b/c", // some child file
|
||||||
|
}
|
||||||
|
g.AddVertex(r1, r2, r3)
|
||||||
|
|
||||||
|
if i := g.NumEdges(); i != 0 {
|
||||||
|
t.Errorf("should have 0 edges instead of: %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
debug := testing.Verbose() // set via the -test.v flag to `go test`
|
||||||
|
logf := func(format string, v ...interface{}) {
|
||||||
|
t.Logf("test: "+format, v...)
|
||||||
|
}
|
||||||
|
// run artificially without the entire engine
|
||||||
|
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
|
||||||
|
t.Errorf("error running autoedges: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// two edges should have been added
|
||||||
|
if i := g.NumEdges(); i != 2 {
|
||||||
|
t.Errorf("should have 2 edges instead of: %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiscEncodeDecode1(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// encode
|
||||||
|
var input interface{} = &FileRes{}
|
||||||
|
b1 := bytes.Buffer{}
|
||||||
|
e := gob.NewEncoder(&b1)
|
||||||
|
err = e.Encode(&input) // pass with &
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Gob failed to Encode: %v", err)
|
||||||
|
}
|
||||||
|
str := base64.StdEncoding.EncodeToString(b1.Bytes())
|
||||||
|
|
||||||
|
// decode
|
||||||
|
var output interface{}
|
||||||
|
bb, err := base64.StdEncoding.DecodeString(str)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Base64 failed to Decode: %v", err)
|
||||||
|
}
|
||||||
|
b2 := bytes.NewBuffer(bb)
|
||||||
|
d := gob.NewDecoder(b2)
|
||||||
|
err = d.Decode(&output) // pass with &
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Gob failed to Decode: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res1, ok := input.(engine.Res)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Input %v is not a Res", res1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res2, ok := output.(engine.Res)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("Output %v is not a Res", res2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := res1.Cmp(res2); err != nil {
|
||||||
|
t.Errorf("The input and output Res values do not match: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMiscEncodeDecode2(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// encode
|
||||||
|
input, err := engine.NewNamedResource("file", "file1")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Can't create: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if err := res1.Cmp(res2); err != nil {
|
||||||
|
t.Errorf("The input and output Res values do not match: %+v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
323
engine/resources/group.go
Normal file
323
engine/resources/group.go
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Group resource.
|
||||||
|
func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err 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 {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *GroupRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare GroupRes to others of the same resource kind
|
||||||
|
res, ok := r.(*GroupRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != res.State {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (obj.GID == nil) != (res.GID == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.GID != nil && res.GID != nil {
|
||||||
|
if *obj.GID != *res.GID {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -1,51 +1,49 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/event"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus"
|
||||||
errwrap "github.com/pkg/errors"
|
errwrap "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrResourceInsufficientParameters is returned when the configuration of the resource
|
|
||||||
// is insufficient for the resource to do any useful work.
|
|
||||||
var ErrResourceInsufficientParameters = errors.New(
|
|
||||||
"Insufficient parameters for this resource")
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(&HostnameRes{})
|
engine.RegisterResource("hostname", func() engine.Res { return &HostnameRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
hostname1Path = "/org/freedesktop/hostname1"
|
hostname1Path = "/org/freedesktop/hostname1"
|
||||||
hostname1Iface = "org.freedesktop.hostname1"
|
hostname1Iface = "org.freedesktop.hostname1"
|
||||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
dbusPropertiesIface = "org.freedesktop.DBus.Properties"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrResourceInsufficientParameters is returned when the configuration of the
|
||||||
|
// resource is insufficient for the resource to do any useful work.
|
||||||
|
var ErrResourceInsufficientParameters = errors.New("insufficient parameters for this resource")
|
||||||
|
|
||||||
// HostnameRes is a resource that allows setting and watching the hostname.
|
// HostnameRes is a resource that allows setting and watching the hostname.
|
||||||
//
|
//
|
||||||
// StaticHostname is the one configured in /etc/hostname or a similar file.
|
// StaticHostname is the one configured in /etc/hostname or a similar file.
|
||||||
@@ -61,7 +59,10 @@ const (
|
|||||||
// Hostname is the fallback value for all 3 fields above, if only Hostname is
|
// Hostname is the fallback value for all 3 fields above, if only Hostname is
|
||||||
// specified, it will set all 3 fields to this value.
|
// specified, it will set all 3 fields to this value.
|
||||||
type HostnameRes struct {
|
type HostnameRes struct {
|
||||||
BaseRes `yaml:",inline"`
|
traits.Base // add the base methods without re-implementation
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
Hostname string `yaml:"hostname"`
|
Hostname string `yaml:"hostname"`
|
||||||
PrettyHostname string `yaml:"pretty_hostname"`
|
PrettyHostname string `yaml:"pretty_hostname"`
|
||||||
StaticHostname string `yaml:"static_hostname"`
|
StaticHostname string `yaml:"static_hostname"`
|
||||||
@@ -70,22 +71,23 @@ type HostnameRes struct {
|
|||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHostnameRes is a constructor for this resource. It also calls Init() for you.
|
// Default returns some sensible defaults for this resource.
|
||||||
func NewHostnameRes(name, staticHostname, transientHostname, prettyHostname string) (*HostnameRes, error) {
|
func (obj *HostnameRes) Default() engine.Res {
|
||||||
obj := &HostnameRes{
|
return &HostnameRes{}
|
||||||
BaseRes: BaseRes{
|
}
|
||||||
Name: name,
|
|
||||||
},
|
// Validate if the params passed in are valid data.
|
||||||
PrettyHostname: prettyHostname,
|
func (obj *HostnameRes) Validate() error {
|
||||||
StaticHostname: staticHostname,
|
if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" {
|
||||||
TransientHostname: transientHostname,
|
return ErrResourceInsufficientParameters
|
||||||
}
|
}
|
||||||
return obj, obj.Init()
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
// Init runs some startup code for this resource.
|
||||||
func (obj *HostnameRes) Init() error {
|
func (obj *HostnameRes) Init(init *engine.Init) error {
|
||||||
obj.BaseRes.kind = "Hostname"
|
obj.init = init // save for later
|
||||||
|
|
||||||
if obj.PrettyHostname == "" {
|
if obj.PrettyHostname == "" {
|
||||||
obj.PrettyHostname = obj.Hostname
|
obj.PrettyHostname = obj.Hostname
|
||||||
}
|
}
|
||||||
@@ -95,94 +97,68 @@ func (obj *HostnameRes) Init() error {
|
|||||||
if obj.TransientHostname == "" {
|
if obj.TransientHostname == "" {
|
||||||
obj.TransientHostname = obj.Hostname
|
obj.TransientHostname = obj.Hostname
|
||||||
}
|
}
|
||||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if the params passed in are valid data.
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
// FIXME: where should this get called ?
|
func (obj *HostnameRes) Close() error {
|
||||||
func (obj *HostnameRes) Validate() error {
|
|
||||||
if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" {
|
|
||||||
return ErrResourceInsufficientParameters
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events.
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
func (obj *HostnameRes) Watch(processChan chan event.Event) error {
|
func (obj *HostnameRes) Watch() error {
|
||||||
if obj.IsWatching() {
|
|
||||||
return nil // TODO: should this be an error?
|
|
||||||
}
|
|
||||||
obj.SetWatching(true)
|
|
||||||
defer obj.SetWatching(false)
|
|
||||||
cuid := obj.converger.Register()
|
|
||||||
defer cuid.Unregister()
|
|
||||||
|
|
||||||
var startup bool
|
|
||||||
Startup := func(block bool) <-chan time.Time {
|
|
||||||
if block {
|
|
||||||
return nil // blocks forever
|
|
||||||
//return make(chan time.Time) // blocks forever
|
|
||||||
}
|
|
||||||
return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we share the bus with others, we will get each others messages!!
|
// if we share the bus with others, we will get each others messages!!
|
||||||
bus, err := util.SystemBusPrivateUsable() // don't share the bus connection!
|
bus, err := util.SystemBusPrivateUsable() // don't share the bus connection!
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrap(err, "Failed to connect to bus")
|
return errwrap.Wrap(err, "Failed to connect to bus")
|
||||||
}
|
}
|
||||||
defer bus.Close()
|
defer bus.Close()
|
||||||
callResult := bus.BusObject().Call(
|
// watch the PropertiesChanged signal on the hostname1 dbus path
|
||||||
"org.freedesktop.DBus.AddMatch", 0,
|
args := fmt.Sprintf(
|
||||||
fmt.Sprintf("type='signal',path='%s',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", hostname1Path))
|
"type='signal', path='%s', interface='%s', member='PropertiesChanged'",
|
||||||
if callResult.Err != nil {
|
hostname1Path,
|
||||||
return errwrap.Wrap(callResult.Err, "Failed to subscribe to DBus events for hostname1")
|
dbusPropertiesIface,
|
||||||
|
)
|
||||||
|
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||||
|
return errwrap.Wrap(call.Err, "Failed to subscribe to DBus events for hostname1")
|
||||||
}
|
}
|
||||||
|
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||||
|
|
||||||
signals := make(chan *dbus.Signal, 10) // closed by dbus package
|
signals := make(chan *dbus.Signal, 10) // closed by dbus package
|
||||||
bus.Signal(signals)
|
bus.Signal(signals)
|
||||||
|
|
||||||
var send = false // send event?
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
for {
|
for {
|
||||||
obj.SetState(ResStateWatching) // reset
|
|
||||||
select {
|
select {
|
||||||
case <-signals:
|
case <-signals:
|
||||||
cuid.SetConverged(false)
|
|
||||||
send = true
|
send = true
|
||||||
obj.StateOK(false) // dirty
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
case event := <-obj.Events():
|
case event, ok := <-obj.init.Events:
|
||||||
cuid.SetConverged(false)
|
if !ok {
|
||||||
// we avoid sending events on unpause
|
return nil
|
||||||
if exit, _ := obj.ReadEvent(&event); exit {
|
}
|
||||||
return nil // exit
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
send = true
|
|
||||||
obj.StateOK(false) // dirty
|
|
||||||
|
|
||||||
case <-cuid.ConvergedTimer():
|
|
||||||
cuid.SetConverged(true) // converged!
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-Startup(startup):
|
|
||||||
cuid.SetConverged(false)
|
|
||||||
send = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do all our event sending all together to avoid duplicate msgs
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
if send {
|
if send {
|
||||||
startup = true // startup finished
|
|
||||||
send = false
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
if exit, err := obj.DoSend(processChan, ""); exit || err != nil {
|
return err // exit if requested
|
||||||
return err // we exit or bubble up a NACK...
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) {
|
func (obj *HostnameRes) updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) {
|
||||||
propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property)
|
propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property)
|
return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property)
|
||||||
@@ -207,7 +183,7 @@ func updateHostnameProperty(object dbus.BusObject, expectedValue, property, sett
|
|||||||
}
|
}
|
||||||
|
|
||||||
// attempting to apply the changes
|
// attempting to apply the changes
|
||||||
log.Printf("Changing %s: %s => %s", property, propertyValue, expectedValue)
|
obj.init.Logf("Changing %s: %s => %s", property, propertyValue, expectedValue)
|
||||||
if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil {
|
if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil {
|
||||||
return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName)
|
return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName)
|
||||||
}
|
}
|
||||||
@@ -228,21 +204,21 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
|
|
||||||
checkOK = true
|
checkOK = true
|
||||||
if obj.PrettyHostname != "" {
|
if obj.PrettyHostname != "" {
|
||||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
|
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
checkOK = checkOK && propertyCheckOK
|
checkOK = checkOK && propertyCheckOK
|
||||||
}
|
}
|
||||||
if obj.StaticHostname != "" {
|
if obj.StaticHostname != "" {
|
||||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
|
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
checkOK = checkOK && propertyCheckOK
|
checkOK = checkOK && propertyCheckOK
|
||||||
}
|
}
|
||||||
if obj.TransientHostname != "" {
|
if obj.TransientHostname != "" {
|
||||||
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
|
propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -252,60 +228,74 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
return checkOK, nil
|
return checkOK, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *HostnameRes) Cmp(r engine.Res) error {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *HostnameRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare HostnameRes to others of the same resource kind
|
||||||
|
res, ok := r.(*HostnameRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.PrettyHostname != res.PrettyHostname {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.StaticHostname != res.StaticHostname {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.TransientHostname != res.TransientHostname {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// HostnameUID is the UID struct for HostnameRes.
|
// HostnameUID is the UID struct for HostnameRes.
|
||||||
type HostnameUID struct {
|
type HostnameUID struct {
|
||||||
BaseUID
|
engine.BaseUID
|
||||||
|
|
||||||
name string
|
name string
|
||||||
prettyHostname string
|
prettyHostname string
|
||||||
staticHostname string
|
staticHostname string
|
||||||
transientHostname string
|
transientHostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoEdges returns the AutoEdge interface. In this case no autoedges are used.
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
func (obj *HostnameRes) AutoEdges() AutoEdge {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetUIDs includes all params to make a unique identification of this object.
|
|
||||||
// Most resources only return one, although some resources can return multiple.
|
// Most resources only return one, although some resources can return multiple.
|
||||||
func (obj *HostnameRes) GetUIDs() []ResUID {
|
func (obj *HostnameRes) UIDs() []engine.ResUID {
|
||||||
x := &HostnameUID{
|
x := &HostnameUID{
|
||||||
BaseUID: BaseUID{name: obj.GetName(), kind: obj.Kind()},
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
name: obj.Name,
|
name: obj.Name(),
|
||||||
prettyHostname: obj.PrettyHostname,
|
prettyHostname: obj.PrettyHostname,
|
||||||
staticHostname: obj.StaticHostname,
|
staticHostname: obj.StaticHostname,
|
||||||
transientHostname: obj.TransientHostname,
|
transientHostname: obj.TransientHostname,
|
||||||
}
|
}
|
||||||
return []ResUID{x}
|
return []engine.ResUID{x}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GroupCmp returns whether two resources can be grouped together or not.
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
func (obj *HostnameRes) GroupCmp(r Res) bool {
|
// It is primarily useful for setting the defaults.
|
||||||
return false
|
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
}
|
type rawRes HostnameRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
def := obj.Default() // get the default
|
||||||
func (obj *HostnameRes) Compare(res Res) bool {
|
res, ok := def.(*HostnameRes) // put in the right format
|
||||||
switch res := res.(type) {
|
if !ok {
|
||||||
// we can only compare HostnameRes to others of the same resource
|
return fmt.Errorf("could not convert to HostnameRes")
|
||||||
case *HostnameRes:
|
|
||||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.Name != res.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.PrettyHostname != res.PrettyHostname {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.StaticHostname != res.StaticHostname {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.TransientHostname != res.TransientHostname {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return true
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = HostnameRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
303
engine/resources/kv.go
Normal file
303
engine/resources/kv.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
|
||||||
|
type KVResSkipCmpStyle int
|
||||||
|
|
||||||
|
// These are the different allowed comparison styles. Most folks will want SkipCmpStyleInt.
|
||||||
|
const (
|
||||||
|
SkipCmpStyleInt KVResSkipCmpStyle = iota
|
||||||
|
SkipCmpStyleString
|
||||||
|
)
|
||||||
|
|
||||||
|
// KVRes is a resource which writes a key/value pair into cluster wide storage.
|
||||||
|
// It will ensure that the key is set to the requested value. The one exception
|
||||||
|
// is that if you use the SkipLessThan parameter, then it will only replace the
|
||||||
|
// stored value with the requested value if it is greater than that stored one.
|
||||||
|
// This allows the KV resource to be used in fast acting, finite state machines
|
||||||
|
// which have monotonically increasing state values that represent progression.
|
||||||
|
// The one exception is that when this resource receives a refresh signal, then
|
||||||
|
// it will set the value to be the exact one if they are not identical already.
|
||||||
|
type KVRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
//traits.Groupable // TODO: it could be useful to group our writes and watches!
|
||||||
|
traits.Refreshable
|
||||||
|
traits.Recvable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// XXX: shouldn't the name be the key?
|
||||||
|
Key string `yaml:"key"` // key to set
|
||||||
|
Value *string `yaml:"value"` // value to set (nil to delete)
|
||||||
|
SkipLessThan bool `yaml:"skiplessthan"` // skip updates as long as stored value is greater
|
||||||
|
SkipCmpStyle KVResSkipCmpStyle `yaml:"skipcmpstyle"` // how to do the less than cmp
|
||||||
|
// TODO: does it make sense to have different backends here? (eg: local)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *KVRes) Default() engine.Res {
|
||||||
|
return &KVRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *KVRes) Validate() error {
|
||||||
|
if obj.Key == "" {
|
||||||
|
return fmt.Errorf("key must not be empty")
|
||||||
|
}
|
||||||
|
if obj.SkipLessThan {
|
||||||
|
if obj.SkipCmpStyle != SkipCmpStyleInt && obj.SkipCmpStyle != SkipCmpStyleString {
|
||||||
|
return fmt.Errorf("the SkipCmpStyle of %v is invalid", obj.SkipCmpStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v := obj.Value; obj.SkipCmpStyle == SkipCmpStyleInt && v != nil {
|
||||||
|
if _, err := strconv.Atoi(*v); err != nil {
|
||||||
|
return fmt.Errorf("the set value of %v can't convert to int", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the resource.
|
||||||
|
func (obj *KVRes) 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 *KVRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *KVRes) Watch() error {
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := obj.init.World.StrMapWatch(obj.Key) // get possible events!
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// NOTE: this part is very similar to the file resource code
|
||||||
|
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!")
|
||||||
|
}
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lessThanCheck checks for less than validity.
|
||||||
|
func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
|
||||||
|
v := *obj.Value
|
||||||
|
if value == v { // redundant check for safety
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||||
|
if !obj.SkipLessThan || refresh { // update lessthan on refresh
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch obj.SkipCmpStyle {
|
||||||
|
case SkipCmpStyleInt:
|
||||||
|
intValue, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
// NOTE: We don't error here since we're going to write
|
||||||
|
// over the value anyways. It could be from an old run!
|
||||||
|
return false, nil // value is bad (old/corrupt), fix it
|
||||||
|
}
|
||||||
|
if vint, err := strconv.Atoi(v); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "can't convert %v to int", v)
|
||||||
|
} else if vint < intValue {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case SkipCmpStyleString:
|
||||||
|
if v < value { // weird way to cmp, but valid
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false, fmt.Errorf("unmatches SkipCmpStyle style %v", obj.SkipCmpStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Password resource. Does nothing, returns happy!
|
||||||
|
func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
obj.init.Logf("CheckApply(%t)", apply)
|
||||||
|
|
||||||
|
if val, exists := obj.init.Recv()["Value"]; exists && val.Changed {
|
||||||
|
// if we received on Value, and it changed, wooo, nothing to do.
|
||||||
|
obj.init.Logf("CheckApply: `Value` was updated!")
|
||||||
|
}
|
||||||
|
|
||||||
|
hostname := obj.init.Hostname // me
|
||||||
|
keyMap, err := obj.init.World.StrMapGet(obj.Key)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "check error during StrGet")
|
||||||
|
}
|
||||||
|
|
||||||
|
if value, ok := keyMap[hostname]; ok && obj.Value != nil {
|
||||||
|
if value == *obj.Value {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := obj.lessThanCheck(value); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if c {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if !ok && obj.Value == nil {
|
||||||
|
return true, nil // nothing to delete, we're good!
|
||||||
|
|
||||||
|
} else if ok && obj.Value == nil { // delete
|
||||||
|
err := obj.init.World.StrMapDel(obj.Key)
|
||||||
|
return false, errwrap.Wrapf(err, "apply error during StrDel")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.init.World.StrMapSet(obj.Key, *obj.Value); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "apply error during StrSet")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *KVRes) Cmp(r engine.Res) error {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *KVRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare KVRes to others of the same resource kind
|
||||||
|
res, ok := r.(*KVRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Key != res.Key {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (obj.Value == nil) != (res.Value == nil) { // xor
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Value != nil && res.Value != nil {
|
||||||
|
if *obj.Value != *res.Value { // compare the strings
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.SkipLessThan != res.SkipLessThan {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.SkipCmpStyle != res.SkipCmpStyle {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// KVUID is the UID struct for KVRes.
|
||||||
|
type KVUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *KVRes) UIDs() []engine.ResUID {
|
||||||
|
x := &KVUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes KVRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*KVRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to KVRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = KVRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
732
engine/resources/mount.go
Normal file
732
engine/resources/mount.go
Normal file
@@ -0,0 +1,732 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
|
"github.com/purpleidea/mgmt/recwatch"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
sdbus "github.com/coreos/go-systemd/dbus"
|
||||||
|
"github.com/coreos/go-systemd/unit"
|
||||||
|
systemdUtil "github.com/coreos/go-systemd/util"
|
||||||
|
fstab "github.com/deniswernert/go-fstab"
|
||||||
|
"github.com/godbus/dbus"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("mount", func() engine.Res { return &MountRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// procFilesystems is a file that lists all the valid filesystem types.
|
||||||
|
procFilesystems = "/proc/filesystems"
|
||||||
|
// procPath is the path to /proc/mounts which contains all active mounts.
|
||||||
|
procPath = "/proc/mounts"
|
||||||
|
// fstabPath is the path to the fstab file which defines mounts.
|
||||||
|
fstabPath = "/etc/fstab"
|
||||||
|
// fstabUmask is the umask (permissions) used to edit /etc/fstab.
|
||||||
|
fstabUmask = 0644
|
||||||
|
|
||||||
|
// getStatus64 is an ioctl command to get the status of file backed
|
||||||
|
// loopback devices (i.e. iso file mounts.)
|
||||||
|
getStatus64 = 0x4C05
|
||||||
|
// loopFileUmask is the umask (permissions) used to read the loop file.
|
||||||
|
loopFileUmask = 0660
|
||||||
|
|
||||||
|
// devDisk is the path where disks and partitions can be found, organized
|
||||||
|
// by uuid/label/path.
|
||||||
|
devDisk = "/dev/disk/"
|
||||||
|
// diskByUUID is the location of symlinks for devices by UUID.
|
||||||
|
diskByUUID = devDisk + "by-uuid/"
|
||||||
|
// diskByLabel is the location of symlinks for devices by label.
|
||||||
|
diskByLabel = devDisk + "by-label/"
|
||||||
|
// diskByUUID is the location of symlinks for partitions by UUID.
|
||||||
|
diskByPartUUID = devDisk + "by-partuuid/"
|
||||||
|
// diskByLabel is the location of symlinks for partitions by label.
|
||||||
|
diskByPartLabel = devDisk + "by-partlabel/"
|
||||||
|
|
||||||
|
// dbusSystemd1Interface is the base systemd1 path.
|
||||||
|
dbusSystemd1Path = "/org/freedesktop/systemd1"
|
||||||
|
// dbusUnitPath is the dbus path where mount unit files are found.
|
||||||
|
dbusUnitPath = dbusSystemd1Path + "/unit/"
|
||||||
|
// dbusSystemd1Interface is the base systemd1 interface.
|
||||||
|
dbusSystemd1Interface = "org.freedesktop.systemd1"
|
||||||
|
// dbusMountInterface is used as an argument to filter dbus messages.
|
||||||
|
dbusMountInterface = dbusSystemd1Interface + ".Mount"
|
||||||
|
// dbusManagerInterface is the systemd manager interface used for
|
||||||
|
// interfacing with systemd units.
|
||||||
|
dbusManagerInterface = dbusSystemd1Interface + ".Manager"
|
||||||
|
// dbusRestartUnit is the dbus method for restarting systemd units.
|
||||||
|
dbusRestartUnit = dbusManagerInterface + ".RestartUnit"
|
||||||
|
// restartTimeout is the delay before restartUnit is assumed to have
|
||||||
|
// failed.
|
||||||
|
dbusRestartCtxTimeout = 10
|
||||||
|
// dbusSignalJobRemoved is the name of the dbus signal that produces a
|
||||||
|
// message when a dbus job is done (or has errored.)
|
||||||
|
dbusSignalJobRemoved = "JobRemoved"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MountRes is a systemd mount resource that adds/removes entries from
|
||||||
|
// /etc/fstab, and makes sure the defined device is mounted or unmounted
|
||||||
|
// accordingly. The mount point is set according to the resource's name.
|
||||||
|
type MountRes struct {
|
||||||
|
traits.Base
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
// State must be exists ot absent. If absent, remaining fields are ignored.
|
||||||
|
State string `yaml:"state"`
|
||||||
|
Device string `yaml:"device"` // location of the device or image
|
||||||
|
Type string `yaml:"type"` // the type of filesystem
|
||||||
|
Options map[string]string `yaml:"options"` // mount options
|
||||||
|
Freq int `yaml:"freq"` // dump frequency
|
||||||
|
PassNo int `yaml:"passno"` // verification order
|
||||||
|
|
||||||
|
mount *fstab.Mount // struct representing the mount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *MountRes) Default() engine.Res {
|
||||||
|
return &MountRes{
|
||||||
|
Options: defaultMntOps(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *MountRes) Validate() error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// validate state
|
||||||
|
if obj.State != "exists" && obj.State != "absent" {
|
||||||
|
return fmt.Errorf("state must be 'exists', or 'absent'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate type
|
||||||
|
fs, err := ioutil.ReadFile(procFilesystems)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error reading %s", procFilesystems)
|
||||||
|
}
|
||||||
|
fsSlice := strings.Fields(string(fs))
|
||||||
|
for i, x := range fsSlice {
|
||||||
|
if x == "nodev" {
|
||||||
|
fsSlice = append(fsSlice[:i], fsSlice[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.State != "absent" && !util.StrInList(obj.Type, fsSlice) {
|
||||||
|
return fmt.Errorf("type must be a valid filesystem type (see /proc/filesystems)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate mountpoint
|
||||||
|
if strings.Contains(obj.Name(), "//") {
|
||||||
|
return fmt.Errorf("double slashes are not allowed in resource name")
|
||||||
|
}
|
||||||
|
if err := unix.Access(obj.Name(), unix.R_OK); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error validating mount point: %s", obj.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate device
|
||||||
|
device, err := evalSpec(obj.Device) // eval symlink
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error evaluating spec: %s", obj.Device)
|
||||||
|
}
|
||||||
|
if err := unix.Access(device, unix.R_OK); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error validating device: %s", device)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *MountRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init //save for later
|
||||||
|
|
||||||
|
obj.mount = &fstab.Mount{
|
||||||
|
Spec: obj.Device,
|
||||||
|
File: obj.Name(),
|
||||||
|
VfsType: obj.Type,
|
||||||
|
MntOps: obj.Options,
|
||||||
|
Freq: obj.Freq,
|
||||||
|
PassNo: obj.PassNo,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *MountRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch listens for signals from the mount unit associated with the resource.
|
||||||
|
// It also watch for changes to /etc/fstab, where mounts are defined.
|
||||||
|
func (obj *MountRes) Watch() error {
|
||||||
|
// make sure systemd is running
|
||||||
|
if !systemdUtil.IsRunningSystemd() {
|
||||||
|
return fmt.Errorf("systemd is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// establish a godbus connection
|
||||||
|
conn, err := util.SystemBusPrivateUsable()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error establishing dbus connection")
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// add a dbus rule to watch signals from the mount unit.
|
||||||
|
args := fmt.Sprintf("type='signal', path='%s', arg0='%s'",
|
||||||
|
dbusUnitPath+sdbus.PathBusEscape(unit.UnitNamePathEscape((obj.Name()+".mount"))),
|
||||||
|
dbusMountInterface,
|
||||||
|
)
|
||||||
|
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||||
|
return errwrap.Wrapf(call.Err, "error creating dbus call")
|
||||||
|
}
|
||||||
|
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||||
|
|
||||||
|
ch := make(chan *dbus.Signal)
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
conn.Signal(ch)
|
||||||
|
defer conn.RemoveSignal(ch)
|
||||||
|
|
||||||
|
// watch the fstab file
|
||||||
|
recWatcher, err := recwatch.NewRecWatcher(fstabPath, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// close the recwatcher when we're done
|
||||||
|
defer recWatcher.Close()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // bubble up a NACK...
|
||||||
|
}
|
||||||
|
|
||||||
|
var send bool
|
||||||
|
var done bool
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-recWatcher.Events():
|
||||||
|
if !ok {
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := event.Error; err != nil {
|
||||||
|
return errwrap.Wrapf(err, "unknown recwatcher error")
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.init.Dirty()
|
||||||
|
send = true
|
||||||
|
|
||||||
|
case event, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("event: %+v", event)
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.init.Dirty()
|
||||||
|
send = true
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fstabCheckApply checks /etc/fstab for entries corresponding to the resource
|
||||||
|
// definition, and adds or deletes the entry as needed.
|
||||||
|
func (obj *MountRes) fstabCheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
exists, err := fstabEntryExists(fstabPath, obj.mount)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking if fstab entry exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if everything is as it should be, we're done
|
||||||
|
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("fstabCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
if obj.State == "exists" {
|
||||||
|
if err := obj.fstabEntryAdd(fstabPath, obj.mount); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error adding fstab entry: %+v", obj.mount)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := obj.fstabEntryRemove(fstabPath, obj.mount); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error removing fstab entry: %+v", obj.mount)
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountCheckApply checks if the defined resource is mounted, and mounts or
|
||||||
|
// unmounts it according to the defined state.
|
||||||
|
func (obj *MountRes) mountCheckApply(apply bool) (bool, error) {
|
||||||
|
exists, err := mountExists(procPath, obj.mount)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking if mount exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if everything is as it should be, we're done
|
||||||
|
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("mountCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
if obj.State == "exists" {
|
||||||
|
// Reload mounts from /etc/fstab by performing a `daemon-reload` and
|
||||||
|
// restarting `local-fs.target` and `remote-fs.target` units.
|
||||||
|
if err := mountReload(); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error reloading /etc/fstab")
|
||||||
|
}
|
||||||
|
return false, nil // we're done
|
||||||
|
}
|
||||||
|
// unmount the device
|
||||||
|
if err := unix.Unmount(obj.Name(), 0); err != nil { // 0 means no flags
|
||||||
|
return false, errwrap.Wrapf(err, "error unmounting %s", obj.Name())
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||||
|
// necessary changes to reach the desired state. This is run before Watch and
|
||||||
|
// again if Watch finds a change occurring to the state.
|
||||||
|
func (obj *MountRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
checkOK = true
|
||||||
|
|
||||||
|
if c, err := obj.fstabCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if c, err := obj.mountCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and return if they are equivalent.
|
||||||
|
func (obj *MountRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare MountRes to others of the same resource kind
|
||||||
|
res, ok := r.(*MountRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
|
}
|
||||||
|
if obj.State != res.State {
|
||||||
|
return fmt.Errorf("the State differs")
|
||||||
|
}
|
||||||
|
if obj.Type != res.Type {
|
||||||
|
return fmt.Errorf("the Type differs")
|
||||||
|
}
|
||||||
|
if !strMapEq(obj.Options, res.Options) {
|
||||||
|
return fmt.Errorf("the Options differ")
|
||||||
|
}
|
||||||
|
if obj.Freq != res.Freq {
|
||||||
|
return fmt.Errorf("the Type differs")
|
||||||
|
}
|
||||||
|
if obj.PassNo != res.PassNo {
|
||||||
|
return fmt.Errorf("the PassNo differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MountUID is a unique resource identifier.
|
||||||
|
type MountUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||||
|
func (obj *MountUID) IFF(uid engine.ResUID) bool {
|
||||||
|
res, ok := uid.(*MountUID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return obj.name == res.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one although some resources can return multiple.
|
||||||
|
func (obj *MountRes) UIDs() []engine.ResUID {
|
||||||
|
x := &MountUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes MountRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*MountRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to MountRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = MountRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultMntOps returns a map that sets the default mount options for fstab
|
||||||
|
// mounts.
|
||||||
|
func defaultMntOps() map[string]string {
|
||||||
|
return map[string]string{"defaults": ""}
|
||||||
|
}
|
||||||
|
|
||||||
|
// strMapEq returns true, if and only if the two provided maps are identical.
|
||||||
|
func strMapEq(x, y map[string]string) bool {
|
||||||
|
if len(x) != len(y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for k, v := range x {
|
||||||
|
if val, ok := x[k]; !ok || v != val {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// fstabEntryExists checks whether or not a given mount exists in the provided
|
||||||
|
// fstab file.
|
||||||
|
func fstabEntryExists(file string, mount *fstab.Mount) (bool, error) {
|
||||||
|
mounts, err := fstab.ParseFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||||
|
}
|
||||||
|
for _, m := range mounts {
|
||||||
|
if m.Equals(mount) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fstabEntryAdd adds the given mount to the provided fstab file.
|
||||||
|
func (obj *MountRes) fstabEntryAdd(file string, mount *fstab.Mount) error {
|
||||||
|
mounts, err := fstab.ParseFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||||
|
}
|
||||||
|
for _, m := range mounts {
|
||||||
|
// if the entry exists, we're done
|
||||||
|
if m.Equals(mount) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// mount does not exist so we need to add it
|
||||||
|
mounts = append(mounts, mount)
|
||||||
|
return obj.fstabWrite(file, mounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fstabEntryRemove removes the given mount from the provided fstab file.
|
||||||
|
func (obj *MountRes) fstabEntryRemove(file string, mount *fstab.Mount) error {
|
||||||
|
mounts, err := fstab.ParseFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||||
|
}
|
||||||
|
for i, m := range mounts {
|
||||||
|
// remove any entry with the defined mountpoint
|
||||||
|
if m.File == mount.File {
|
||||||
|
mounts = append(mounts[:i], mounts[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return obj.fstabWrite(file, mounts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fstabWrite generates an fstab file with the given mounts, and writes them
|
||||||
|
// to the provided fstab file.
|
||||||
|
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
|
||||||
|
// build the file contents
|
||||||
|
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
|
||||||
|
contents = contents + mounts.String() + "\n"
|
||||||
|
// write the file
|
||||||
|
if err := ioutil.WriteFile(file, []byte(contents), fstabUmask); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error writing fstab file: %s", file)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountExists returns true, if a given mount exists in the given file
|
||||||
|
// (typically /proc/mounts.)
|
||||||
|
func mountExists(file string, mount *fstab.Mount) (bool, error) {
|
||||||
|
var err error
|
||||||
|
m := *mount // make a copy so we don't change the definition
|
||||||
|
|
||||||
|
// resolve the device's symlink if there is one
|
||||||
|
if m.Spec, err = evalSpec(mount.Spec); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error evaluating spec: %s", mount.Spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all mounts
|
||||||
|
mounts, err := fstab.ParseFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
|
||||||
|
}
|
||||||
|
// check for the defined mount
|
||||||
|
for _, p := range mounts {
|
||||||
|
found, err := mountCompare(&m, p)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "mounts could not be compared: %s and %s", mount.String(), p.String())
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountCompare compares two mounts. It is assumed that the first comes from
|
||||||
|
// a resource definition, and the second comes from /proc/mounts. It compares
|
||||||
|
// the two after resolving the loopback device's file path (if necessary,) and
|
||||||
|
// ignores freq and passno, as they may differ between the definition and
|
||||||
|
// /proc/mounts.
|
||||||
|
func mountCompare(def, proc *fstab.Mount) (bool, error) {
|
||||||
|
if def.Equals(proc) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if def.File != proc.File {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if def.Spec != "" {
|
||||||
|
procSpec, err := loopFilePath(proc.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if def.Spec != procSpec {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strMapEq(def.MntOps, defaultMntOps()) && !strMapEq(def.MntOps, proc.MntOps) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if def.VfsType != "" && def.VfsType != proc.VfsType {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mountReload performs a daemon-reload and restarts fs-local.target and
|
||||||
|
// fs-remote.target, to let systemd mount any new entries in /etc/fstab.
|
||||||
|
func mountReload() error {
|
||||||
|
// establish a godbus connection
|
||||||
|
conn, err := util.SystemBusPrivateUsable()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error establishing dbus connection")
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
// systemctl daemon-reload
|
||||||
|
conn.BusObject().Call("Reload", 0)
|
||||||
|
|
||||||
|
// systemctl restart local-fs.target
|
||||||
|
if err := restartUnit(conn, "local-fs.target"); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error restarting unit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemctl restart remote-fs.target
|
||||||
|
if err := restartUnit(conn, "local-fs.target"); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error restarting unit")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// restartUnit restarts the given dbus unit and waits for it to finish
|
||||||
|
// starting up. If restartTimeout is exceeded, it will return an error.
|
||||||
|
func restartUnit(conn *dbus.Conn, unit string) error {
|
||||||
|
// timeout if we don't get the JobRemoved event
|
||||||
|
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Add a dbus rule to watch the systemd1 JobRemoved signal used to wait
|
||||||
|
// until the restart job completes.
|
||||||
|
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s', arg2='%s'",
|
||||||
|
dbusSystemd1Path,
|
||||||
|
dbusManagerInterface,
|
||||||
|
dbusSignalJobRemoved,
|
||||||
|
unit,
|
||||||
|
)
|
||||||
|
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||||
|
return errwrap.Wrapf(call.Err, "error creating dbus call")
|
||||||
|
}
|
||||||
|
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||||
|
|
||||||
|
// channel for godbus connection
|
||||||
|
ch := make(chan *dbus.Signal)
|
||||||
|
defer close(ch)
|
||||||
|
|
||||||
|
conn.Signal(ch)
|
||||||
|
defer conn.RemoveSignal(ch)
|
||||||
|
|
||||||
|
// restart the unit
|
||||||
|
sd1 := conn.Object(dbusSystemd1Interface, dbus.ObjectPath(dbusSystemd1Path))
|
||||||
|
if call := sd1.Call(dbusRestartUnit, 0, unit, "fail"); call.Err != nil {
|
||||||
|
return errwrap.Wrapf(call.Err, "error restarting unit: %s", unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for the job to be removed, indicating completion
|
||||||
|
select {
|
||||||
|
case event, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("channel closed unexpectedly")
|
||||||
|
}
|
||||||
|
if event.Body[3] != "done" {
|
||||||
|
return fmt.Errorf("unexpected job status: %s", event.Body[3])
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return fmt.Errorf("restarting %s failed due to context timeout", unit)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// evalSpec resolves the device from the supplied spec, i.e. it follows the
|
||||||
|
// symlink, if any, from the provided uuid, label, or path.
|
||||||
|
func evalSpec(spec string) (string, error) {
|
||||||
|
var path string
|
||||||
|
m := &fstab.Mount{}
|
||||||
|
m.Spec = spec
|
||||||
|
|
||||||
|
switch m.SpecType() {
|
||||||
|
case fstab.UUID:
|
||||||
|
path = diskByUUID + m.SpecValue()
|
||||||
|
case fstab.Label:
|
||||||
|
path = diskByLabel + m.SpecValue()
|
||||||
|
case fstab.PartUUID:
|
||||||
|
path = diskByPartUUID + m.SpecValue()
|
||||||
|
case fstab.PartLabel:
|
||||||
|
path = diskByPartLabel + m.SpecValue()
|
||||||
|
case fstab.Path:
|
||||||
|
path = m.SpecValue()
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("unexpected spec type: %v", m.SpecType())
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.EvalSymlinks(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loopFilePath returns the file path of the mounted filesystem image, backing
|
||||||
|
// the given loopback device.
|
||||||
|
func loopFilePath(spec string) (string, error) {
|
||||||
|
// if it's not a loopback device, return the input
|
||||||
|
if !strings.Contains(spec, "/dev/loop") {
|
||||||
|
return spec, nil
|
||||||
|
}
|
||||||
|
info, err := getLoopInfo(spec)
|
||||||
|
if err != nil {
|
||||||
|
return "", errwrap.Wrapf(err, "error getting loop info")
|
||||||
|
}
|
||||||
|
// trim the extra null chars off the end of the filename
|
||||||
|
return string(bytes.Trim(info.FileName[:], "\x00")), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// loopInfo is a datastructure that holds relevant information about a file
|
||||||
|
// backed loopback device. Code is based on freddierice/go-losetup.
|
||||||
|
type loopInfo struct {
|
||||||
|
Device uint64
|
||||||
|
INode uint64
|
||||||
|
RDevice uint64
|
||||||
|
Offset uint64
|
||||||
|
SizeLimit uint64
|
||||||
|
Number uint32
|
||||||
|
EncryptType uint32
|
||||||
|
EncryptKeySize uint32
|
||||||
|
Flags uint32
|
||||||
|
FileName [64]byte
|
||||||
|
CryptName [64]byte
|
||||||
|
EncryptKey [32]byte
|
||||||
|
Init [2]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLoopInfo returns a loopInfo struct containing information about the
|
||||||
|
// provided file backed loopback device.
|
||||||
|
func getLoopInfo(loop string) (*loopInfo, error) {
|
||||||
|
// open the loop file
|
||||||
|
f, err := os.OpenFile(loop, 0, loopFileUmask)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error opening %s: %s", loop, err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// deserialize the contents
|
||||||
|
retInfo := &loopInfo{}
|
||||||
|
_, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), getStatus64, uintptr(unsafe.Pointer(retInfo)))
|
||||||
|
if errno == unix.ENXIO {
|
||||||
|
return nil, fmt.Errorf("device not backed by a file")
|
||||||
|
} else if errno != 0 {
|
||||||
|
return nil, fmt.Errorf("error getting info about %s (errno: %d)", loop, errno)
|
||||||
|
}
|
||||||
|
|
||||||
|
return retInfo, nil
|
||||||
|
}
|
||||||
343
engine/resources/mount_test.go
Normal file
343
engine/resources/mount_test.go
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
fstab "github.com/deniswernert/go-fstab"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fstabMock1 = `UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad / ext4 defaults 1 1` + "\n"
|
||||||
|
|
||||||
|
const procMock1 = `/tmp/mount0 /mnt/proctest ext4 rw,seclabel,relatime,data=ordered 0 0` + "\n"
|
||||||
|
|
||||||
|
var fstabWriteTests = []struct {
|
||||||
|
in fstab.Mounts
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fstab.Mounts{
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||||
|
File: "/boot",
|
||||||
|
VfsType: "ext3",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 2,
|
||||||
|
},
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/mapper/home",
|
||||||
|
File: "/home",
|
||||||
|
VfsType: "ext3",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fstab.Mounts{
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/cdrom",
|
||||||
|
File: "/mnt/cdrom",
|
||||||
|
VfsType: "iso9660",
|
||||||
|
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *MountRes) TestFstabWrite(t *testing.T) {
|
||||||
|
file, err := ioutil.TempFile("", "fstab")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating temp file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
|
for _, test := range fstabWriteTests {
|
||||||
|
if err := obj.fstabWrite(file.Name(), test.in); err != nil {
|
||||||
|
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, mount := range test.in {
|
||||||
|
exists, err := fstabEntryExists(file.Name(), mount)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error checking if fstab entry %s exists: %v", mount.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("failed to write %s to fstab", mount.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fstabEntryAddTests = []struct {
|
||||||
|
fstabMock []byte
|
||||||
|
in *fstab.Mount
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]byte(fstabMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/sdb1",
|
||||||
|
File: "/mnt/foo",
|
||||||
|
VfsType: "ext2",
|
||||||
|
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]byte(fstabMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||||
|
File: "/",
|
||||||
|
VfsType: "ext3",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *MountRes) TestFstabEntryAdd(t *testing.T) {
|
||||||
|
file, err := ioutil.TempFile("", "fstab")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating temp file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
|
for _, test := range fstabEntryAddTests {
|
||||||
|
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||||
|
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := obj.fstabEntryAdd(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error adding fstab entry: %s to file: %s: %v", test.in.String(), file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exists, err := fstabEntryExists(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
t.Errorf("fstab failed to add entry: %s to fstab", test.in.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fstabEntryRemoveTests = []struct {
|
||||||
|
fstabMock []byte
|
||||||
|
in *fstab.Mount
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]byte(fstabMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
|
||||||
|
File: "/",
|
||||||
|
VfsType: "ext4",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *MountRes) TestFstabEntryRemove(t *testing.T) {
|
||||||
|
file, err := ioutil.TempFile("", "fstab")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating temp file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
|
for _, test := range fstabEntryRemoveTests {
|
||||||
|
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||||
|
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err := obj.fstabEntryRemove(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error removing fstab entry: %s from file: %s: %v", test.in.String(), file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exists, err := fstabEntryExists(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
t.Errorf("fstab failed to remove entry: %s from fstab", test.in.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mountCompareTests = []struct {
|
||||||
|
dIn *fstab.Mount
|
||||||
|
pIn *fstab.Mount
|
||||||
|
out bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/foo",
|
||||||
|
File: "/mnt/foo",
|
||||||
|
VfsType: "ext3",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
},
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/foo",
|
||||||
|
File: "/mnt/foo",
|
||||||
|
VfsType: "ext3",
|
||||||
|
MntOps: map[string]string{"foo": "bar", "baz": ""},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||||
|
File: "/mnt/foo",
|
||||||
|
VfsType: "ext3",
|
||||||
|
},
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
|
||||||
|
File: "/mnt/bar",
|
||||||
|
VfsType: "ext3",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var fstabEntryExistsTests = []struct {
|
||||||
|
fstabMock []byte
|
||||||
|
in *fstab.Mount
|
||||||
|
out bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]byte(fstabMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
|
||||||
|
File: "/",
|
||||||
|
VfsType: "ext4",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 1,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[]byte(fstabMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/dev/mapper/root",
|
||||||
|
File: "/home",
|
||||||
|
VfsType: "ext4",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 1,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFstabEntryExists(t *testing.T) {
|
||||||
|
file, err := ioutil.TempFile("", "fstab")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating temp file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
|
||||||
|
for _, test := range fstabEntryExistsTests {
|
||||||
|
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
|
||||||
|
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := fstabEntryExists(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result != test.out {
|
||||||
|
t.Errorf("fstabEntryExists test wanted: %t, got: %t", test.out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountCompare(t *testing.T) {
|
||||||
|
for _, test := range mountCompareTests {
|
||||||
|
result, err := mountCompare(test.dIn, test.pIn)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error comparing mounts: %s and %s: %v", test.dIn.String(), test.pIn.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result != test.out {
|
||||||
|
t.Errorf("mountCompare test wanted: %t, got: %t", test.out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mountExistsTests = []struct {
|
||||||
|
procMock []byte
|
||||||
|
in *fstab.Mount
|
||||||
|
out bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
[]byte(procMock1),
|
||||||
|
&fstab.Mount{
|
||||||
|
Spec: "/tmp/mount0",
|
||||||
|
File: "/mnt/proctest",
|
||||||
|
VfsType: "ext4",
|
||||||
|
MntOps: map[string]string{"defaults": ""},
|
||||||
|
Freq: 1,
|
||||||
|
PassNo: 1,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMountExists(t *testing.T) {
|
||||||
|
file, err := ioutil.TempFile("", "proc")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error creating temp file: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file.Name())
|
||||||
|
for _, test := range mountExistsTests {
|
||||||
|
if err := ioutil.WriteFile(file.Name(), test.procMock, 0664); err != nil {
|
||||||
|
t.Errorf("error writing proc file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ioutil.WriteFile(test.in.Spec, []byte{}, 0664); err != nil {
|
||||||
|
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := mountExists(file.Name(), test.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if result != test.out {
|
||||||
|
t.Errorf("mountExistsTests test wanted: %t, got: %t", test.out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +1,44 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/event"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
|
||||||
"github.com/coreos/go-systemd/journal"
|
"github.com/coreos/go-systemd/journal"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(&MsgRes{})
|
engine.RegisterResource("msg", func() engine.Res { return &MsgRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgRes is a resource that writes messages to logs.
|
// MsgRes is a resource that writes messages to logs.
|
||||||
type MsgRes struct {
|
type MsgRes struct {
|
||||||
BaseRes `yaml:",inline"`
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Refreshable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
Body string `yaml:"body"`
|
Body string `yaml:"body"`
|
||||||
Priority string `yaml:"priority"`
|
Priority string `yaml:"priority"`
|
||||||
Fields map[string]string `yaml:"fields"`
|
Fields map[string]string `yaml:"fields"`
|
||||||
@@ -47,54 +49,80 @@ type MsgRes struct {
|
|||||||
syslogStateOK bool
|
syslogStateOK bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// MsgUID is a unique representation for a MsgRes object.
|
// Default returns some sensible defaults for this resource.
|
||||||
type MsgUID struct {
|
func (obj *MsgRes) Default() engine.Res {
|
||||||
BaseUID
|
return &MsgRes{}
|
||||||
body string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMsgRes is a constructor for this resource.
|
// Validate the params that are passed to MsgRes.
|
||||||
func NewMsgRes(name, body, priority string, journal, syslog bool, fields map[string]string) (*MsgRes, error) {
|
|
||||||
message := name
|
|
||||||
if body != "" {
|
|
||||||
message = body
|
|
||||||
}
|
|
||||||
|
|
||||||
obj := &MsgRes{
|
|
||||||
BaseRes: BaseRes{
|
|
||||||
Name: name,
|
|
||||||
},
|
|
||||||
Body: message,
|
|
||||||
Priority: priority,
|
|
||||||
Fields: fields,
|
|
||||||
Journal: journal,
|
|
||||||
Syslog: syslog,
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj, obj.Init()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init runs some startup code for this resource.
|
|
||||||
func (obj *MsgRes) Init() error {
|
|
||||||
obj.BaseRes.kind = "Msg"
|
|
||||||
return obj.BaseRes.Init() // call base init, b/c we're overrriding
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the params that are passed to MsgRes
|
|
||||||
func (obj *MsgRes) Validate() error {
|
func (obj *MsgRes) Validate() error {
|
||||||
invalidCharacters := regexp.MustCompile("[^a-zA-Z0-9_]")
|
invalidCharacters := regexp.MustCompile("[^a-zA-Z0-9_]")
|
||||||
for field := range obj.Fields {
|
for field := range obj.Fields {
|
||||||
if invalidCharacters.FindString(field) != "" {
|
if invalidCharacters.FindString(field) != "" {
|
||||||
return fmt.Errorf("Invalid character in field %s.", field)
|
return fmt.Errorf("invalid character in field %s", field)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(field, "_") {
|
if strings.HasPrefix(field, "_") {
|
||||||
return fmt.Errorf("Fields cannot begin with _.")
|
return fmt.Errorf("fields cannot begin with _")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switch obj.Priority {
|
||||||
|
case "Emerg":
|
||||||
|
case "Alert":
|
||||||
|
case "Crit":
|
||||||
|
case "Err":
|
||||||
|
case "Warning":
|
||||||
|
case "Notice":
|
||||||
|
case "Info":
|
||||||
|
case "Debug":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid Priority '%s'", obj.Priority)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isAllStateOK derives a compound state from all internal cache flags that apply to this resource.
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *MsgRes) 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 *MsgRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *MsgRes) Watch() error {
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAllStateOK derives a compound state from all internal cache flags that
|
||||||
|
// apply to this resource.
|
||||||
func (obj *MsgRes) isAllStateOK() bool {
|
func (obj *MsgRes) isAllStateOK() bool {
|
||||||
if obj.Journal && !obj.journalStateOK {
|
if obj.Journal && !obj.journalStateOK {
|
||||||
return false
|
return false
|
||||||
@@ -107,11 +135,13 @@ func (obj *MsgRes) isAllStateOK() bool {
|
|||||||
|
|
||||||
// updateStateOK sets the global state so it can be read by the engine.
|
// updateStateOK sets the global state so it can be read by the engine.
|
||||||
func (obj *MsgRes) updateStateOK() {
|
func (obj *MsgRes) updateStateOK() {
|
||||||
obj.StateOK(obj.isAllStateOK())
|
// XXX: this resource doesn't entirely make sense to me at the moment.
|
||||||
|
if !obj.isAllStateOK() {
|
||||||
|
obj.init.Dirty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// JournalPriority converts a string description to a numeric priority.
|
// JournalPriority converts a string description to a numeric priority.
|
||||||
// XXX: Have Validate() make sure it actually is one of these.
|
|
||||||
func (obj *MsgRes) journalPriority() journal.Priority {
|
func (obj *MsgRes) journalPriority() journal.Priority {
|
||||||
switch obj.Priority {
|
switch obj.Priority {
|
||||||
case "Emerg":
|
case "Emerg":
|
||||||
@@ -134,69 +164,15 @@ func (obj *MsgRes) journalPriority() journal.Priority {
|
|||||||
return journal.PriNotice
|
return journal.PriNotice
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events.
|
// CheckApply method for Msg resource. Every check leads to an apply, meaning
|
||||||
func (obj *MsgRes) Watch(processChan chan event.Event) error {
|
// that the message is flushed to the journal.
|
||||||
if obj.IsWatching() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
obj.SetWatching(true)
|
|
||||||
defer obj.SetWatching(false)
|
|
||||||
cuid := obj.converger.Register()
|
|
||||||
defer cuid.Unregister()
|
|
||||||
|
|
||||||
var startup bool
|
|
||||||
Startup := func(block bool) <-chan time.Time {
|
|
||||||
if block {
|
|
||||||
return nil // blocks forever
|
|
||||||
//return make(chan time.Time) // blocks forever
|
|
||||||
}
|
|
||||||
return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
var send = false // send event?
|
|
||||||
var exit = false
|
|
||||||
for {
|
|
||||||
obj.SetState(ResStateWatching) // reset
|
|
||||||
select {
|
|
||||||
case event := <-obj.Events():
|
|
||||||
cuid.SetConverged(false)
|
|
||||||
// we avoid sending events on unpause
|
|
||||||
if exit, send = obj.ReadEvent(&event); exit {
|
|
||||||
return nil // exit
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-cuid.ConvergedTimer():
|
|
||||||
cuid.SetConverged(true) // converged!
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-Startup(startup):
|
|
||||||
cuid.SetConverged(false)
|
|
||||||
send = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// do all our event sending all together to avoid duplicate msgs
|
|
||||||
if send {
|
|
||||||
startup = true // startup finished
|
|
||||||
send = false
|
|
||||||
// only do this on certain types of events
|
|
||||||
//obj.isStateOK = false // something made state dirty
|
|
||||||
if exit, err := obj.DoSend(processChan, ""); exit || err != nil {
|
|
||||||
return err // we exit or bubble up a NACK...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckApply method for Msg resource.
|
|
||||||
// Every check leads to an apply, meaning that the message is flushed to the journal.
|
|
||||||
func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||||
|
|
||||||
// isStateOK() done by engine, so we updateStateOK() to pass in value
|
// isStateOK() done by engine, so we updateStateOK() to pass in value
|
||||||
//if obj.isAllStateOK() {
|
//if obj.isAllStateOK() {
|
||||||
// return true, nil
|
// return true, nil
|
||||||
//}
|
//}
|
||||||
|
|
||||||
if obj.Refresh() { // if we were notified...
|
if obj.init.Refresh() { // if we were notified...
|
||||||
// invalidate cached state...
|
// invalidate cached state...
|
||||||
obj.logStateOK = false
|
obj.logStateOK = false
|
||||||
if obj.Journal {
|
if obj.Journal {
|
||||||
@@ -209,7 +185,7 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !obj.logStateOK {
|
if !obj.logStateOK {
|
||||||
log.Printf("%s[%s]: Body: %s", obj.Kind(), obj.GetName(), obj.Body)
|
obj.init.Logf("Body: %s", obj.Body)
|
||||||
obj.logStateOK = true
|
obj.logStateOK = true
|
||||||
obj.updateStateOK()
|
obj.updateStateOK()
|
||||||
}
|
}
|
||||||
@@ -232,48 +208,73 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUIDs includes all params to make a unique identification of this object.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
// Most resources only return one, although some resources can return multiple.
|
func (obj *MsgRes) Cmp(r engine.Res) error {
|
||||||
func (obj *MsgRes) GetUIDs() []ResUID {
|
if !obj.Compare(r) {
|
||||||
x := &MsgUID{
|
return fmt.Errorf("did not compare")
|
||||||
BaseUID: BaseUID{
|
|
||||||
name: obj.GetName(),
|
|
||||||
kind: obj.Kind(),
|
|
||||||
},
|
|
||||||
body: obj.Body,
|
|
||||||
}
|
}
|
||||||
return []ResUID{x}
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoEdges returns the AutoEdges. In this case none are used.
|
|
||||||
func (obj *MsgRes) AutoEdges() AutoEdge {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
// Compare two resources and return if they are equivalent.
|
||||||
func (obj *MsgRes) Compare(res Res) bool {
|
func (obj *MsgRes) Compare(r engine.Res) bool {
|
||||||
switch res.(type) {
|
// we can only compare MsgRes to others of the same resource kind
|
||||||
case *MsgRes:
|
res, ok := r.(*MsgRes)
|
||||||
res := res.(*MsgRes)
|
if !ok {
|
||||||
if !obj.BaseRes.Compare(res) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.Body != res.Body {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.Priority != res.Priority {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(obj.Fields) != len(res.Fields) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for field, value := range obj.Fields {
|
|
||||||
if res.Fields[field] != value {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obj.Body != res.Body {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Priority != res.Priority {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(obj.Fields) != len(res.Fields) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for field, value := range obj.Fields {
|
||||||
|
if res.Fields[field] != value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MsgUID is a unique representation for a MsgRes object.
|
||||||
|
type MsgUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
|
||||||
|
body string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *MsgRes) UIDs() []engine.ResUID {
|
||||||
|
x := &MsgUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
body: obj.Body,
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes MsgRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*MsgRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to MsgRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = MsgRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
engine/resources/msg_test.go
Normal file
44
engine/resources/msg_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMsgValidate1(t *testing.T) {
|
||||||
|
r1 := &MsgRes{
|
||||||
|
Priority: "Debug",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r1.Validate(); err != nil {
|
||||||
|
t.Errorf("validate failed with: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMsgValidate2(t *testing.T) {
|
||||||
|
r1 := &MsgRes{
|
||||||
|
Priority: "UnrealPriority",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r1.Validate(); err == nil {
|
||||||
|
t.Errorf("validation error is nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
889
engine/resources/net.go
Normal file
889
engine/resources/net.go
Normal file
@@ -0,0 +1,889 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !darwin
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/recwatch"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
// XXX: Do NOT use subscribe methods from this lib, as they are racey and
|
||||||
|
// do not clean up spawned goroutines. Should be replaced when a suitable
|
||||||
|
// alternative is available.
|
||||||
|
"github.com/vishvananda/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("net", func() engine.Res { return &NetRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// IfacePrefix is the prefix used to identify unit files for managed links.
|
||||||
|
IfacePrefix = "mgmt-"
|
||||||
|
// networkdUnitFileDir is the location of networkd unit files which define
|
||||||
|
// the systemd network connections.
|
||||||
|
networkdUnitFileDir = "/etc/systemd/network/"
|
||||||
|
// networkdUnitFileExt is the file extension for networkd unit files.
|
||||||
|
networkdUnitFileExt = ".network"
|
||||||
|
// networkdUnitFileUmask sets the permissions on the systemd unit file.
|
||||||
|
networkdUnitFileUmask = 0644
|
||||||
|
|
||||||
|
// ifaceUp is the up (on) interface state.
|
||||||
|
ifaceUp = "up"
|
||||||
|
// ifaceDown is the down (off) interface state.
|
||||||
|
ifaceDown = "down"
|
||||||
|
|
||||||
|
// Netlink multicast groups to watch for events. For all groups see:
|
||||||
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
||||||
|
rtmGrps = rtmGrpLink | rtmGrpIPv4IfAddr | rtmGrpIPv6IfAddr | rtmGrpIPv4IfRoute
|
||||||
|
rtmGrpLink = 0x1 // interface create/delete/up/down
|
||||||
|
rtmGrpIPv4IfAddr = 0x10 // add/delete IPv4 addresses
|
||||||
|
rtmGrpIPv6IfAddr = 0x100 // add/delete IPv6 addresses
|
||||||
|
rtmGrpIPv4IfRoute = 0x40 // add delete routes
|
||||||
|
|
||||||
|
// IP routing protocols for used for netlink route messages. For all
|
||||||
|
// protocols see:
|
||||||
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
|
||||||
|
rtProtoKernel = 2 // kernel
|
||||||
|
rtProtoStatic = 4 // static
|
||||||
|
|
||||||
|
socketFile = "pipe.sock" // path in vardir to store our socket file
|
||||||
|
)
|
||||||
|
|
||||||
|
// NetRes is a network interface resource based on netlink. It manages the
|
||||||
|
// state of a network link. Configuration is also stored in a networkd
|
||||||
|
// configuration file, so the network is available upon reboot.
|
||||||
|
type NetRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
State string `yaml:"state"` // up, down, or empty
|
||||||
|
Addrs []string `yaml:"addrs"` // list of addresses in cidr format
|
||||||
|
Gateway string `yaml:"gateway"` // gateway address
|
||||||
|
|
||||||
|
iface *iface // a struct containing the net.Interface and netlink.Link
|
||||||
|
unitFilePath string // the interface unit file path
|
||||||
|
|
||||||
|
socketFile string // path for storing the pipe socket file
|
||||||
|
}
|
||||||
|
|
||||||
|
// nlChanStruct defines the channel used to send netlink messages and errors
|
||||||
|
// to the event processing loop in Watch.
|
||||||
|
type nlChanStruct struct {
|
||||||
|
msg []syscall.NetlinkMessage
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *NetRes) Default() engine.Res {
|
||||||
|
return &NetRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *NetRes) Validate() error {
|
||||||
|
// validate state
|
||||||
|
if obj.State != ifaceUp && obj.State != ifaceDown && obj.State != "" {
|
||||||
|
return fmt.Errorf("state must be up, down or empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate network address input
|
||||||
|
if obj.Addrs != nil {
|
||||||
|
for _, addr := range obj.Addrs {
|
||||||
|
if _, _, err := net.ParseCIDR(addr); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing address: %s", addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.Gateway != "" {
|
||||||
|
if g := net.ParseIP(obj.Gateway); g == nil {
|
||||||
|
return fmt.Errorf("error parsing gateway: %s", obj.Gateway)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate the interface name
|
||||||
|
_, err := net.InterfaceByName(obj.Name())
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *NetRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// tmp directory for pipe socket
|
||||||
|
dir, err := obj.init.VarDir("")
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||||
|
}
|
||||||
|
obj.socketFile = path.Join(dir, socketFile) // return a unique file
|
||||||
|
|
||||||
|
// store the network interface in the struct
|
||||||
|
obj.iface = &iface{}
|
||||||
|
if obj.iface.iface, err = net.InterfaceByName(obj.Name()); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
|
||||||
|
}
|
||||||
|
// store the netlink link to use as interface input in netlink functions
|
||||||
|
if obj.iface.link, err = netlink.LinkByName(obj.Name()); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error finding link: %s", obj.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the path to the networkd configuration file
|
||||||
|
obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.Name() + networkdUnitFileExt
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close cleans up when we're done.
|
||||||
|
func (obj *NetRes) Close() error {
|
||||||
|
var errList error
|
||||||
|
|
||||||
|
if obj.socketFile == "/" {
|
||||||
|
return fmt.Errorf("socket file should not be the root path")
|
||||||
|
}
|
||||||
|
if obj.socketFile != "" { // safety
|
||||||
|
if err := os.Remove(obj.socketFile); err != nil {
|
||||||
|
errList = multierr.Append(errList, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch listens for events from the specified interface via a netlink socket.
|
||||||
|
// TODO: currently gets events from ALL interfaces, would be nice to reject
|
||||||
|
// events from other interfaces.
|
||||||
|
func (obj *NetRes) Watch() error {
|
||||||
|
// waitgroup for netlink receive goroutine
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
// create a netlink socket for receiving network interface events
|
||||||
|
conn, err := newSocketSet(rtmGrps, obj.socketFile)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error creating socket set")
|
||||||
|
}
|
||||||
|
defer conn.shutdown() // close the netlink socket and unblock conn.receive()
|
||||||
|
|
||||||
|
// watch the systemd-networkd configuration file
|
||||||
|
recWatcher, err := recwatch.NewRecWatcher(obj.unitFilePath, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// close the recwatcher when we're done
|
||||||
|
defer recWatcher.Close()
|
||||||
|
|
||||||
|
// channel for netlink messages
|
||||||
|
nlChan := make(chan *nlChanStruct) // closed from goroutine
|
||||||
|
|
||||||
|
// channel to unblock selects in goroutine
|
||||||
|
closeChan := make(chan struct{})
|
||||||
|
defer close(closeChan)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer conn.close() // close the pipe when we're done with it
|
||||||
|
defer close(nlChan)
|
||||||
|
for {
|
||||||
|
// receive messages from the socket set
|
||||||
|
msgs, err := conn.receive()
|
||||||
|
if err != nil {
|
||||||
|
select {
|
||||||
|
case nlChan <- &nlChanStruct{
|
||||||
|
err: errwrap.Wrapf(err, "error receiving messages"),
|
||||||
|
}:
|
||||||
|
case <-closeChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case nlChan <- &nlChanStruct{
|
||||||
|
msg: msgs,
|
||||||
|
}:
|
||||||
|
case <-closeChan:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
var done bool
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case s, ok := <-nlChan:
|
||||||
|
if !ok {
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := s.err; err != nil {
|
||||||
|
return errwrap.Wrapf(s.err, "unknown netlink error")
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Event: %+v", s.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-recWatcher.Events():
|
||||||
|
if !ok {
|
||||||
|
if done {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
done = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := event.Error; err != nil {
|
||||||
|
return errwrap.Wrapf(err, "unknown recwatcher error")
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||||
|
}
|
||||||
|
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ifaceCheckApply checks the state of the network device and brings it up or
|
||||||
|
// down as necessary.
|
||||||
|
func (obj *NetRes) ifaceCheckApply(apply bool) (bool, error) {
|
||||||
|
// check the interface state
|
||||||
|
state, err := obj.iface.state()
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking %s state", obj.Name())
|
||||||
|
}
|
||||||
|
// if the state is correct or unspecified, we're done
|
||||||
|
if obj.State == state || obj.State == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of state checking
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("ifaceCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
// ip link set up/down
|
||||||
|
if err := obj.iface.linkUpDown(obj.State); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error setting %s up or down", obj.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrCheckApply checks if the interface has the correct addresses and then
|
||||||
|
// adds/deletes addresses as necessary.
|
||||||
|
func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
|
||||||
|
// get the link's addresses
|
||||||
|
ifaceAddrs, err := obj.iface.getAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.Name())
|
||||||
|
}
|
||||||
|
// if state is not defined
|
||||||
|
if obj.Addrs == nil {
|
||||||
|
// send addrs
|
||||||
|
obj.Addrs = ifaceAddrs
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// check if all addrs have a kernel route needed for first hop
|
||||||
|
kernelOK, err := obj.iface.kernelCheck(obj.Addrs)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking kernel routes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the kernel routes are intact and the addrs match, we're done
|
||||||
|
err = util.SortedStrSliceCompare(obj.Addrs, ifaceAddrs)
|
||||||
|
if err == nil && kernelOK {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of state checking
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("addrCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
// check each address and delete the ones that aren't in the definition
|
||||||
|
if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking or deleting addresses")
|
||||||
|
}
|
||||||
|
// check each address and add the ones that are defined but do not exist
|
||||||
|
if err := obj.iface.addrApplyAdd(obj.Addrs); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking or adding addresses")
|
||||||
|
}
|
||||||
|
// make sure all the addrs have the appropriate kernel routes
|
||||||
|
if err := obj.iface.kernelApply(obj.Addrs); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error adding kernel routes")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// gatewayCheckApply checks if the interface has the correct default gateway
|
||||||
|
// and adds/deletes routes as necessary.
|
||||||
|
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
|
||||||
|
// get all routes from the interface
|
||||||
|
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error getting default routes")
|
||||||
|
}
|
||||||
|
// add default routes to a slice
|
||||||
|
defRoutes := []netlink.Route{}
|
||||||
|
for _, route := range routes {
|
||||||
|
if route.Dst == nil { // route is default
|
||||||
|
defRoutes = append(defRoutes, route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the gateway is already set, we're done
|
||||||
|
if len(defRoutes) == 1 && defRoutes[0].Gw.String() == obj.Gateway {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
// if no gateway was defined
|
||||||
|
if obj.Gateway == "" {
|
||||||
|
// send the gateway if there is one
|
||||||
|
if len(defRoutes) == 1 {
|
||||||
|
obj.Gateway = defRoutes[0].Gw.String()
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of state checking
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("gatewayCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
// delete all but one default route
|
||||||
|
for i := 1; i < len(defRoutes); i++ {
|
||||||
|
if err := netlink.RouteDel(&defRoutes[i]); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error deleting route: %+v", defRoutes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add or change the default route
|
||||||
|
if err := netlink.RouteReplace(&netlink.Route{
|
||||||
|
LinkIndex: obj.iface.iface.Index,
|
||||||
|
Gw: net.ParseIP(obj.Gateway),
|
||||||
|
Protocol: rtProtoStatic,
|
||||||
|
}); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error replacing default route")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileCheckApply checks and maintains the systemd-networkd unit file contents.
|
||||||
|
func (obj *NetRes) fileCheckApply(apply bool) (bool, error) {
|
||||||
|
// check if the unit file exists
|
||||||
|
_, err := os.Stat(obj.unitFilePath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return false, errwrap.Wrapf(err, "error checking file")
|
||||||
|
}
|
||||||
|
// build the unit file contents from the definition
|
||||||
|
contents := obj.unitFileContents()
|
||||||
|
// check the file contents
|
||||||
|
if err == nil {
|
||||||
|
unitFile, err := ioutil.ReadFile(obj.unitFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error reading file")
|
||||||
|
}
|
||||||
|
// return if the file is good
|
||||||
|
if bytes.Equal(unitFile, contents) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
obj.init.Logf("fileCheckApply(%t)", apply)
|
||||||
|
|
||||||
|
// write the file
|
||||||
|
if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error writing configuration file")
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||||
|
// necessary changes to reach the desired state. This is run before Watch and
|
||||||
|
// again if Watch finds a change occurring to the state.
|
||||||
|
func (obj *NetRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
checkOK = true
|
||||||
|
|
||||||
|
// check the network device
|
||||||
|
if c, err := obj.ifaceCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the interface is supposed to be down, we're done
|
||||||
|
if obj.State == ifaceDown {
|
||||||
|
return checkOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the addresses
|
||||||
|
if c, err := obj.addrCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the gateway
|
||||||
|
if c, err := obj.gatewayCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the state is unspecified, we're done
|
||||||
|
if obj.State == "" {
|
||||||
|
return checkOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the networkd unit file
|
||||||
|
if c, err := obj.fileCheckApply(apply); err != nil {
|
||||||
|
return false, err
|
||||||
|
} else if !c {
|
||||||
|
checkOK = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *NetRes) Cmp(r engine.Res) error {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *NetRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare NetRes to others of the same resource kind
|
||||||
|
res, ok := r.(*NetRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != res.State {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (obj.Addrs == nil) != (res.Addrs == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.Gateway != res.Gateway {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetUID is a unique resource identifier.
|
||||||
|
type NetUID struct {
|
||||||
|
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||||
|
// information about where this UID came from, and is unrelated to the
|
||||||
|
// information about the resource we're matching. That data which is
|
||||||
|
// used in the IFF function, is what you see in the struct fields here.
|
||||||
|
engine.BaseUID
|
||||||
|
|
||||||
|
name string // the network interface name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||||
|
func (obj *NetUID) IFF(uid engine.ResUID) bool {
|
||||||
|
res, ok := uid.(*NetUID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return obj.name == res.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one although some resources can return multiple.
|
||||||
|
func (obj *NetRes) UIDs() []engine.ResUID {
|
||||||
|
x := &NetUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes NetRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*NetRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to NetRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = NetRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unitFileContents builds the unit file contents from the definition.
|
||||||
|
func (obj *NetRes) unitFileContents() []byte {
|
||||||
|
// build the unit file contents
|
||||||
|
u := []string{"[Match]"}
|
||||||
|
u = append(u, fmt.Sprintf("Name=%s", obj.Name()))
|
||||||
|
u = append(u, "[Network]")
|
||||||
|
for _, addr := range obj.Addrs {
|
||||||
|
u = append(u, fmt.Sprintf("Address=%s", addr))
|
||||||
|
}
|
||||||
|
if obj.Gateway != "" {
|
||||||
|
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
|
||||||
|
}
|
||||||
|
c := strings.Join(u, "\n")
|
||||||
|
return []byte(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// iface wraps net.Interface to add additional methods.
|
||||||
|
type iface struct {
|
||||||
|
iface *net.Interface
|
||||||
|
link netlink.Link
|
||||||
|
}
|
||||||
|
|
||||||
|
// state reports the state of the interface as up or down.
|
||||||
|
func (obj *iface) state() (string, error) {
|
||||||
|
var err error
|
||||||
|
if obj.iface, err = net.InterfaceByName(obj.iface.Name); err != nil {
|
||||||
|
return "", errwrap.Wrapf(err, "error updating interface")
|
||||||
|
}
|
||||||
|
// if the interface's "up" flag is 0, it's down
|
||||||
|
if obj.iface.Flags&net.FlagUp == 0 {
|
||||||
|
return ifaceDown, nil
|
||||||
|
}
|
||||||
|
// otherwise it's up
|
||||||
|
return ifaceUp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// linkUpDown brings the interface up or down, depending on input value.
|
||||||
|
func (obj *iface) linkUpDown(state string) error {
|
||||||
|
if state != ifaceUp && state != ifaceDown {
|
||||||
|
return fmt.Errorf("state must be up or down")
|
||||||
|
}
|
||||||
|
if state == ifaceUp {
|
||||||
|
return netlink.LinkSetUp(obj.link)
|
||||||
|
}
|
||||||
|
return netlink.LinkSetDown(obj.link)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAddrs returns a list of strings containing all of the interface's
|
||||||
|
// IP addresses in CIDR format.
|
||||||
|
func (obj *iface) getAddrs() ([]string, error) {
|
||||||
|
var ifaceAddrs []string
|
||||||
|
a, err := obj.iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||||
|
}
|
||||||
|
// we're only interested in the strings (not the network)
|
||||||
|
for _, addr := range a {
|
||||||
|
ifaceAddrs = append(ifaceAddrs, addr.String())
|
||||||
|
}
|
||||||
|
return ifaceAddrs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// kernelCheck checks if all addresses in the list have a corresponding kernel
|
||||||
|
// route, without which the network would be unreachable.
|
||||||
|
func (obj *iface) kernelCheck(addrs []string) (bool, error) {
|
||||||
|
var routeOK bool
|
||||||
|
|
||||||
|
// get a list of all the routes associated with the interface
|
||||||
|
routes, err := netlink.RouteList(obj.link, netlink.FAMILY_V4)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error getting routes")
|
||||||
|
}
|
||||||
|
// check each route against each addr
|
||||||
|
for _, addr := range addrs {
|
||||||
|
routeOK = false
|
||||||
|
ip, ipNet, err := net.ParseCIDR(addr)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "error parsing addr: %s", addr)
|
||||||
|
}
|
||||||
|
for _, r := range routes {
|
||||||
|
// if src, dst and protocol are correct, the kernel route exists
|
||||||
|
if r.Src.Equal(ip) && r.Dst.String() == ipNet.String() && r.Protocol == rtProtoKernel {
|
||||||
|
routeOK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if any addr is missing a kernel route return early
|
||||||
|
if !routeOK {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return routeOK, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// kernelApply adds or replaces each address' kernel route as necessary.
|
||||||
|
func (obj *iface) kernelApply(addrs []string) error {
|
||||||
|
// for each addr, add or replace the corresponding kernel route
|
||||||
|
for _, addr := range addrs {
|
||||||
|
ip, ipNet, err := net.ParseCIDR(addr)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing addr: %s", addr)
|
||||||
|
}
|
||||||
|
// kernel route needed for the network to be reachable from a given ip
|
||||||
|
if err := netlink.RouteReplace(&netlink.Route{
|
||||||
|
LinkIndex: obj.iface.Index,
|
||||||
|
Dst: ipNet,
|
||||||
|
Src: ip,
|
||||||
|
Protocol: rtProtoKernel,
|
||||||
|
Scope: netlink.SCOPE_LINK,
|
||||||
|
}); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error replacing first hop route")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrApplyDelete, checks the interface's addresses and deletes any that are not
|
||||||
|
// in the list/definition.
|
||||||
|
func (obj *iface) addrApplyDelete(objAddrs []string) error {
|
||||||
|
ifaceAddrs, err := obj.getAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||||
|
}
|
||||||
|
for _, ifaceAddr := range ifaceAddrs {
|
||||||
|
addrOK := false
|
||||||
|
for _, objAddr := range objAddrs {
|
||||||
|
if ifaceAddr == objAddr {
|
||||||
|
addrOK = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := netlink.ParseAddr(ifaceAddr)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing netlink address: %s", ifaceAddr)
|
||||||
|
}
|
||||||
|
if err := netlink.AddrDel(obj.link, addr); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error deleting addr: %s from %s", ifaceAddr, obj.iface.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrApplyAdd checks if the interface has each address in the supplied list,
|
||||||
|
// and if it doesn't, it adds them.
|
||||||
|
func (obj *iface) addrApplyAdd(objAddrs []string) error {
|
||||||
|
ifaceAddrs, err := obj.getAddrs()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
|
||||||
|
}
|
||||||
|
for _, objAddr := range objAddrs {
|
||||||
|
addrOK := false
|
||||||
|
for _, ifaceAddr := range ifaceAddrs {
|
||||||
|
if ifaceAddr == objAddr {
|
||||||
|
addrOK = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
addr, err := netlink.ParseAddr(objAddr)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error parsing cidr address: %s", objAddr)
|
||||||
|
}
|
||||||
|
if err := netlink.AddrAdd(obj.link, addr); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error adding addr: %s to %s", objAddr, obj.iface.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// socketSet is used to receive events from a socket and shut it down cleanly
|
||||||
|
// when asked. It contains a socket for events and a pipe socket to unblock
|
||||||
|
// receive on shutdown.
|
||||||
|
type socketSet struct {
|
||||||
|
fdEvents int
|
||||||
|
fdPipe int
|
||||||
|
pipeFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSocketSet returns a socketSet, initialized with the given parameters.
|
||||||
|
func newSocketSet(groups uint32, file string) (*socketSet, error) {
|
||||||
|
// make a netlink socket file descriptor
|
||||||
|
fdEvents, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_ROUTE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error creating netlink socket")
|
||||||
|
}
|
||||||
|
// bind to the socket and add add the netlink groups we need to get events
|
||||||
|
if err := unix.Bind(fdEvents, &unix.SockaddrNetlink{
|
||||||
|
Family: unix.AF_NETLINK,
|
||||||
|
Groups: groups,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error binding netlink socket")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a pipe socket to unblock unix.Select when we close
|
||||||
|
fdPipe, err := unix.Socket(unix.AF_UNIX, unix.SOCK_RAW, unix.PROT_NONE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error creating pipe socket")
|
||||||
|
}
|
||||||
|
// bind the pipe to a file
|
||||||
|
if err = unix.Bind(fdPipe, &unix.SockaddrUnix{
|
||||||
|
Name: file,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error binding pipe socket")
|
||||||
|
}
|
||||||
|
return &socketSet{
|
||||||
|
fdEvents: fdEvents,
|
||||||
|
fdPipe: fdPipe,
|
||||||
|
pipeFile: file,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// shutdown closes the event file descriptor and unblocks receive by sending
|
||||||
|
// a message to the pipe file descriptor. It must be called before close, and
|
||||||
|
// should only be called once.
|
||||||
|
func (obj *socketSet) shutdown() error {
|
||||||
|
// close the event socket so no more events are produced
|
||||||
|
if err := unix.Close(obj.fdEvents); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// send a message to the pipe to unblock select
|
||||||
|
return unix.Sendto(obj.fdPipe, nil, 0, &unix.SockaddrUnix{
|
||||||
|
Name: path.Join(obj.pipeFile),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// close closes the pipe file descriptor. It must only be called after
|
||||||
|
// shutdown has closed fdEvents, and unblocked receive. It should only be
|
||||||
|
// called once.
|
||||||
|
func (obj *socketSet) close() error {
|
||||||
|
return unix.Close(obj.fdPipe)
|
||||||
|
}
|
||||||
|
|
||||||
|
// receive waits for bytes from fdEvents and parses them into a slice of
|
||||||
|
// netlink messages. It will block until an event is produced, or shutdown
|
||||||
|
// is called.
|
||||||
|
func (obj *socketSet) receive() ([]syscall.NetlinkMessage, error) {
|
||||||
|
// Select will return when any fd in fdSet (fdEvents and fdPipe) is ready
|
||||||
|
// to read.
|
||||||
|
_, err := unix.Select(obj.nfd(), obj.fdSet(), nil, nil, nil)
|
||||||
|
if err != nil {
|
||||||
|
// if a system interrupt is caught
|
||||||
|
if err == unix.EINTR { // signal interrupt
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errwrap.Wrapf(err, "error selecting on fd")
|
||||||
|
}
|
||||||
|
// receive the message from the netlink socket into b
|
||||||
|
b := make([]byte, os.Getpagesize())
|
||||||
|
n, _, err := unix.Recvfrom(obj.fdEvents, b, unix.MSG_DONTWAIT) // non-blocking receive
|
||||||
|
if err != nil {
|
||||||
|
// if fdEvents is closed
|
||||||
|
if err == unix.EBADF { // bad file descriptor
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errwrap.Wrapf(err, "error receiving messages")
|
||||||
|
}
|
||||||
|
// if we didn't get enough bytes for a header, something went wrong
|
||||||
|
if n < unix.NLMSG_HDRLEN {
|
||||||
|
return nil, fmt.Errorf("received short header")
|
||||||
|
}
|
||||||
|
b = b[:n] // truncate b to message length
|
||||||
|
// use syscall to parse, as func does not exist in x/sys/unix
|
||||||
|
return syscall.ParseNetlinkMessage(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// nfd returns one more than the highest fd value in the struct, for use as as
|
||||||
|
// the nfds parameter in select. It represents the file descriptor set maximum
|
||||||
|
// size. See man select for more info.
|
||||||
|
func (obj *socketSet) nfd() int {
|
||||||
|
if obj.fdEvents > obj.fdPipe {
|
||||||
|
return obj.fdEvents + 1
|
||||||
|
}
|
||||||
|
return obj.fdPipe + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// fdSet returns a bitmask representation of the integer values of fdEvents
|
||||||
|
// and fdPipe. See man select for more info.
|
||||||
|
func (obj *socketSet) fdSet() *unix.FdSet {
|
||||||
|
fdSet := &unix.FdSet{}
|
||||||
|
// Generate the bitmask representing the file descriptors in the socketSet.
|
||||||
|
// The rightmost bit corresponds to file descriptor zero, and each bit to
|
||||||
|
// the left represents the next file descriptor number in the sequence of
|
||||||
|
// all real numbers. E.g. the FdSet containing containing 0 and 4 is 10001.
|
||||||
|
fdSet.Bits[obj.fdEvents/64] |= 1 << uint(obj.fdEvents)
|
||||||
|
fdSet.Bits[obj.fdPipe/64] |= 1 << uint(obj.fdPipe)
|
||||||
|
return fdSet
|
||||||
|
}
|
||||||
166
engine/resources/net_test.go
Normal file
166
engine/resources/net_test.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// test cases for NetRes.unitFileContents()
|
||||||
|
var unitFileContentsTests = []struct {
|
||||||
|
dev string
|
||||||
|
in *NetRes
|
||||||
|
out []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"eth0",
|
||||||
|
&NetRes{
|
||||||
|
State: "up",
|
||||||
|
Addrs: []string{"192.168.42.13/24"},
|
||||||
|
Gateway: "192.168.42.1",
|
||||||
|
},
|
||||||
|
[]byte(
|
||||||
|
strings.Join(
|
||||||
|
[]string{
|
||||||
|
"[Match]",
|
||||||
|
"Name=eth0",
|
||||||
|
"[Network]",
|
||||||
|
"Address=192.168.42.13/24",
|
||||||
|
"Gateway=192.168.42.1",
|
||||||
|
},
|
||||||
|
"\n"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"wlp5s0",
|
||||||
|
&NetRes{
|
||||||
|
State: "up",
|
||||||
|
Addrs: []string{"10.0.2.13/24", "10.0.2.42/24"},
|
||||||
|
Gateway: "10.0.2.1",
|
||||||
|
},
|
||||||
|
[]byte(
|
||||||
|
strings.Join(
|
||||||
|
[]string{
|
||||||
|
"[Match]",
|
||||||
|
"Name=wlp5s0",
|
||||||
|
"[Network]",
|
||||||
|
"Address=10.0.2.13/24",
|
||||||
|
"Address=10.0.2.42/24",
|
||||||
|
"Gateway=10.0.2.1",
|
||||||
|
},
|
||||||
|
"\n"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test NetRes.unitFileContents()
|
||||||
|
func TestUnitFileContents(t *testing.T) {
|
||||||
|
for _, test := range unitFileContentsTests {
|
||||||
|
test.in.SetName(test.dev)
|
||||||
|
result := test.in.unitFileContents()
|
||||||
|
if !bytes.Equal(test.out, result) {
|
||||||
|
t.Errorf("nfd test wanted:\n %s, got:\n %s", test.out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test cases for socketSet.fdSet()
|
||||||
|
var fdSetTests = []struct {
|
||||||
|
in *socketSet
|
||||||
|
out *unix.FdSet
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 3,
|
||||||
|
fdPipe: 4,
|
||||||
|
},
|
||||||
|
&unix.FdSet{
|
||||||
|
Bits: [16]int64{0x18}, // 11000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 12,
|
||||||
|
fdPipe: 8,
|
||||||
|
},
|
||||||
|
&unix.FdSet{
|
||||||
|
Bits: [16]int64{0x1100}, // 1000100000000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 9,
|
||||||
|
fdPipe: 21,
|
||||||
|
},
|
||||||
|
&unix.FdSet{
|
||||||
|
Bits: [16]int64{0x200200}, // 1000000000001000000000
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test socketSet.fdSet()
|
||||||
|
func TestFdSet(t *testing.T) {
|
||||||
|
for _, test := range fdSetTests {
|
||||||
|
result := test.in.fdSet()
|
||||||
|
if *result != *test.out {
|
||||||
|
t.Errorf("fdSet test wanted: %b, got: %b", *test.out, *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test cases for socketSet.nfd()
|
||||||
|
var nfdTests = []struct {
|
||||||
|
in *socketSet
|
||||||
|
out int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 3,
|
||||||
|
fdPipe: 4,
|
||||||
|
},
|
||||||
|
5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 8,
|
||||||
|
fdPipe: 4,
|
||||||
|
},
|
||||||
|
9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&socketSet{
|
||||||
|
fdEvents: 90,
|
||||||
|
fdPipe: 900,
|
||||||
|
},
|
||||||
|
901,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// test socketSet.nfd()
|
||||||
|
func TestNfd(t *testing.T) {
|
||||||
|
for _, test := range nfdTests {
|
||||||
|
result := test.in.nfd()
|
||||||
|
if result != test.out {
|
||||||
|
t.Errorf("nfd test wanted: %d, got: %d", test.out, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
engine/resources/noop.go
Normal file
163
engine/resources/noop.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("noop", func() engine.Res { return &NoopRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoopRes is a no-op resource that does nothing.
|
||||||
|
type NoopRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Groupable
|
||||||
|
traits.Refreshable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
Comment string `lang:"comment" yaml:"comment"` // extra field for example purposes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *NoopRes) Default() engine.Res {
|
||||||
|
return &NoopRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *NoopRes) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *NoopRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *NoopRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *NoopRes) Watch() error {
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Noop resource. Does nothing, returns happy!
|
||||||
|
func (obj *NoopRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
if obj.init.Refresh() {
|
||||||
|
obj.init.Logf("received a notification!")
|
||||||
|
}
|
||||||
|
return true, nil // state is always okay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *NoopRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare NoopRes to others of the same resource kind
|
||||||
|
res, ok := r.(*NoopRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("not a %s", obj.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Comment != res.Comment {
|
||||||
|
return fmt.Errorf("the Comment differs")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NoopUID is the UID struct for NoopRes.
|
||||||
|
type NoopUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *NoopRes) UIDs() []engine.ResUID {
|
||||||
|
x := &NoopUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupCmp returns whether two resources can be grouped together or not.
|
||||||
|
func (obj *NoopRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
|
_, ok := r.(*NoopRes)
|
||||||
|
if !ok {
|
||||||
|
// NOTE: technically we could group a noop into any other
|
||||||
|
// resource, if that resource knew how to handle it, although,
|
||||||
|
// since the mechanics of inter-kind resource grouping are
|
||||||
|
// tricky, avoid doing this until there's a good reason.
|
||||||
|
return fmt.Errorf("resource is not the same kind")
|
||||||
|
}
|
||||||
|
return nil // noop resources can always be grouped together!
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *NoopRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes NoopRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*NoopRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to NoopRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = NoopRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
104
engine/resources/noop_test.go
Normal file
104
engine/resources/noop_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCmp1(t *testing.T) {
|
||||||
|
r1, err := engine.NewResource("noop")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not create resource: %+v", err)
|
||||||
|
}
|
||||||
|
r2, err := engine.NewResource("noop")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not create resource: %+v", err)
|
||||||
|
}
|
||||||
|
r3, err := engine.NewResource("file")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("could not create resource: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := r1.Cmp(r2); err != nil {
|
||||||
|
t.Errorf("the two resources do not match: %+v", err)
|
||||||
|
}
|
||||||
|
if err := r2.Cmp(r1); err != nil {
|
||||||
|
t.Errorf("the two resources do not match: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r1.Cmp(r3) == nil {
|
||||||
|
t.Errorf("the two resources should not match")
|
||||||
|
}
|
||||||
|
if r3.Cmp(r1) == nil {
|
||||||
|
t.Errorf("the two resources should not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSort0(t *testing.T) {
|
||||||
|
rs := []engine.Res{}
|
||||||
|
s := engine.Sort(rs)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(s, []engine.Res{}) {
|
||||||
|
t.Errorf("sort failed!")
|
||||||
|
if s == nil {
|
||||||
|
t.Logf("output is nil!")
|
||||||
|
} else {
|
||||||
|
str := "Got:"
|
||||||
|
for _, r := range s {
|
||||||
|
str += " " + r.String()
|
||||||
|
}
|
||||||
|
t.Errorf(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSort1(t *testing.T) {
|
||||||
|
r1, _ := engine.NewNamedResource("noop", "noop1")
|
||||||
|
r2, _ := engine.NewNamedResource("noop", "noop2")
|
||||||
|
r3, _ := engine.NewNamedResource("noop", "noop3")
|
||||||
|
r4, _ := engine.NewNamedResource("noop", "noop4")
|
||||||
|
r5, _ := engine.NewNamedResource("noop", "noop5")
|
||||||
|
r6, _ := engine.NewNamedResource("noop", "noop6")
|
||||||
|
|
||||||
|
rs := []engine.Res{r3, r2, r6, r1, r5, r4}
|
||||||
|
s := engine.Sort(rs)
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(s, []engine.Res{r1, r2, r3, r4, r5, r6}) {
|
||||||
|
t.Errorf("sort failed!")
|
||||||
|
str := "Got:"
|
||||||
|
for _, r := range s {
|
||||||
|
str += " " + r.String()
|
||||||
|
}
|
||||||
|
t.Errorf(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(rs, []engine.Res{r3, r2, r6, r1, r5, r4}) {
|
||||||
|
t.Errorf("sort modified input!")
|
||||||
|
str := "Got:"
|
||||||
|
for _, r := range rs {
|
||||||
|
str += " " + r.String()
|
||||||
|
}
|
||||||
|
t.Errorf(str)
|
||||||
|
}
|
||||||
|
}
|
||||||
382
engine/resources/nspawn.go
Normal file
382
engine/resources/nspawn.go
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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 (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
systemdDbus "github.com/coreos/go-systemd/dbus"
|
||||||
|
machined "github.com/coreos/go-systemd/machine1"
|
||||||
|
systemdUtil "github.com/coreos/go-systemd/util"
|
||||||
|
"github.com/godbus/dbus"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
running = "running"
|
||||||
|
stopped = "stopped"
|
||||||
|
dbusMachine1Iface = "org.freedesktop.machine1.Manager"
|
||||||
|
machineNew = dbusMachine1Iface + ".MachineNew"
|
||||||
|
machineRemoved = dbusMachine1Iface + ".MachineRemoved"
|
||||||
|
nspawnServiceTmpl = "systemd-nspawn@%s"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("nspawn", func() engine.Res { return &NspawnRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// NspawnRes is an nspawn container resource.
|
||||||
|
type NspawnRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
//traits.Groupable // TODO: this would be quite useful for this resource
|
||||||
|
traits.Refreshable // needed because we embed a svc res
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
State string `yaml:"state"`
|
||||||
|
// We're using the svc resource to start and stop the machine because
|
||||||
|
// that's what machinectl does. We're not using svc.Watch because then we
|
||||||
|
// would have two watches potentially racing each other and producing
|
||||||
|
// potentially unexpected results. We get everything we need to monitor
|
||||||
|
// the machine state changes from the org.freedesktop.machine1 object.
|
||||||
|
svc *SvcRes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *NspawnRes) Default() engine.Res {
|
||||||
|
return &NspawnRes{
|
||||||
|
State: running,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeComposite creates a pointer to a SvcRes. The pointer is used to
|
||||||
|
// validate and initialize the nested svc.
|
||||||
|
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
||||||
|
res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
svc := res.(*SvcRes)
|
||||||
|
svc.State = obj.State
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *NspawnRes) Validate() error {
|
||||||
|
if len(obj.Name()) > 64 {
|
||||||
|
return fmt.Errorf("name must be 64 characters or less")
|
||||||
|
}
|
||||||
|
// check if systemd version is higher than 231 to allow non-alphanumeric
|
||||||
|
// machine names, as previous versions would error in such cases
|
||||||
|
ver, err := systemdVersion()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if ver < 231 {
|
||||||
|
for _, char := range obj.Name() {
|
||||||
|
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
|
||||||
|
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != running && obj.State != stopped {
|
||||||
|
return fmt.Errorf("invalid state: %s", obj.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := obj.makeComposite()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
||||||
|
}
|
||||||
|
if err := svc.Validate(); err != nil { // composite resource
|
||||||
|
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *NspawnRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
svc, err := obj.makeComposite()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "makeComposite failed in init")
|
||||||
|
}
|
||||||
|
obj.svc = svc
|
||||||
|
// TODO: we could build a new init that adds a prefix to the logger...
|
||||||
|
if err := obj.svc.Init(init); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *NspawnRes) Close() error {
|
||||||
|
if obj.svc != nil {
|
||||||
|
return obj.svc.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for state changes and sends a message to the bus if there is a change.
|
||||||
|
func (obj *NspawnRes) Watch() error {
|
||||||
|
// this resource depends on systemd to ensure that it's running
|
||||||
|
if !systemdUtil.IsRunningSystemd() {
|
||||||
|
return fmt.Errorf("systemd is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a private message bus
|
||||||
|
bus, err := util.SystemBusPrivateUsable()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "failed to connect to bus")
|
||||||
|
}
|
||||||
|
defer bus.Close()
|
||||||
|
|
||||||
|
// add a match rule to match messages going through the message bus
|
||||||
|
args := fmt.Sprintf("type='signal',interface='%s',eavesdrop='true'", dbusMachine1Iface)
|
||||||
|
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
||||||
|
|
||||||
|
busChan := make(chan *dbus.Signal)
|
||||||
|
defer close(busChan)
|
||||||
|
bus.Signal(busChan)
|
||||||
|
defer bus.RemoveSignal(busChan) // not needed here, but nice for symmetry
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event := <-busChan:
|
||||||
|
// process org.freedesktop.machine1 events for this resource's name
|
||||||
|
if event.Body[0] == obj.Name() {
|
||||||
|
obj.init.Logf("Event received: %v", event.Name)
|
||||||
|
if event.Name == machineNew {
|
||||||
|
obj.init.Logf("Machine started")
|
||||||
|
} else if event.Name == machineRemoved {
|
||||||
|
obj.init.Logf("Machine stopped")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unknown event: %s", event.Name)
|
||||||
|
}
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
}
|
||||||
|
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply is run to check the state and, if apply is true, to apply the
|
||||||
|
// necessary changes to reach the desired state. This is run before Watch and
|
||||||
|
// again if Watch finds a change occurring to the state.
|
||||||
|
func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
// this resource depends on systemd to ensure that it's running
|
||||||
|
if !systemdUtil.IsRunningSystemd() {
|
||||||
|
return false, errors.New("systemd is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to org.freedesktop.machine1.Manager
|
||||||
|
conn, err := machined.New()
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "failed to connect to dbus")
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the current state with the desired state and perform the
|
||||||
|
// appropriate action
|
||||||
|
var exists = true
|
||||||
|
properties, err := conn.DescribeMachine(obj.Name())
|
||||||
|
if err != nil {
|
||||||
|
if err, ok := err.(dbus.Error); ok && err.Name !=
|
||||||
|
"org.freedesktop.machine1.NoSuchMachine" {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
exists = false
|
||||||
|
// if we could not successfully get the properties because
|
||||||
|
// there's no such machine the machine is stopped
|
||||||
|
// error if we need the image ignore if we don't
|
||||||
|
if _, err = conn.GetImage(obj.Name()); err != nil && obj.State != stopped {
|
||||||
|
return false, fmt.Errorf(
|
||||||
|
"no machine nor image named '%s'",
|
||||||
|
obj.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("properties: %v", properties)
|
||||||
|
}
|
||||||
|
// if the machine doesn't exist and is supposed to
|
||||||
|
// be stopped or the state matches we're done
|
||||||
|
if !exists && obj.State == stopped || properties["State"] == obj.State {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("CheckApply() in valid state")
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of state checking. if we're here, checkOK is false
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.init.Logf("CheckApply() applying '%s' state", obj.State)
|
||||||
|
// use the embedded svc to apply the correct state
|
||||||
|
if _, err := obj.svc.CheckApply(apply); err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "nested svc failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *NspawnRes) Cmp(r engine.Res) error {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *NspawnRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare NspawnRes to others of the same resource kind
|
||||||
|
res, ok := r.(*NspawnRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != res.State {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: why is res.svc ever nil?
|
||||||
|
if (obj.svc == nil) != (res.svc == nil) { // xor
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.svc != nil && res.svc != nil {
|
||||||
|
if !obj.svc.Compare(res.svc) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// NspawnUID is a unique resource identifier.
|
||||||
|
type NspawnUID struct {
|
||||||
|
// NOTE: There is also a name variable in the BaseUID struct, this is
|
||||||
|
// information about where this UID came from, and is unrelated to the
|
||||||
|
// information about the resource we're matching. That data which is
|
||||||
|
// used in the IFF function, is what you see in the struct fields here.
|
||||||
|
engine.BaseUID
|
||||||
|
|
||||||
|
name string // the machine name
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||||
|
func (obj *NspawnUID) IFF(uid engine.ResUID) bool {
|
||||||
|
res, ok := uid.(*NspawnUID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return obj.name == res.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one although some resources can return multiple.
|
||||||
|
func (obj *NspawnRes) UIDs() []engine.ResUID {
|
||||||
|
x := &NspawnUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(), // svc name
|
||||||
|
}
|
||||||
|
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes NspawnRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*NspawnRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to NspawnRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = NspawnRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// systemdVersion uses dbus to check which version of systemd is installed.
|
||||||
|
func systemdVersion() (uint16, error) {
|
||||||
|
// check if systemd is running
|
||||||
|
if !systemdUtil.IsRunningSystemd() {
|
||||||
|
return 0, fmt.Errorf("systemd is not running")
|
||||||
|
}
|
||||||
|
bus, err := systemdDbus.NewSystemdConnection()
|
||||||
|
if err != nil {
|
||||||
|
return 0, errwrap.Wrapf(err, "failed to connect to bus")
|
||||||
|
}
|
||||||
|
defer bus.Close()
|
||||||
|
// get the systemd version
|
||||||
|
verString, err := bus.GetManagerProperty("Version")
|
||||||
|
if err != nil {
|
||||||
|
return 0, errwrap.Wrapf(err, "could not get version property")
|
||||||
|
}
|
||||||
|
// lose the surrounding quotes
|
||||||
|
verNum, err := strconv.Unquote(verString)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errwrap.Wrapf(err, "error unquoting version number")
|
||||||
|
}
|
||||||
|
// cast to uint16
|
||||||
|
ver, err := strconv.ParseUint(verNum, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return 0, errwrap.Wrapf(err, "error casting systemd version number")
|
||||||
|
}
|
||||||
|
return uint16(ver), nil
|
||||||
|
}
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
// Package packagekit provides an interface to interact with packagekit.
|
// Package packagekit provides an interface to interact with packagekit.
|
||||||
@@ -22,19 +22,20 @@ package packagekit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||||
"github.com/purpleidea/mgmt/util"
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
"github.com/godbus/dbus"
|
"github.com/godbus/dbus"
|
||||||
|
multierr "github.com/hashicorp/go-multierror"
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// global tweaks of verbosity and code path
|
// global tweaks of verbosity and code path
|
||||||
const (
|
const (
|
||||||
PK_DEBUG = false
|
Paranoid = false // enable if you see any ghosts
|
||||||
PARANOID = false // enable if you see any ghosts
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants which might need to be tweaked or which contain special dbus strings.
|
// constants which might need to be tweaked or which contain special dbus strings.
|
||||||
@@ -47,7 +48,6 @@ const (
|
|||||||
PkPath = "/org/freedesktop/PackageKit"
|
PkPath = "/org/freedesktop/PackageKit"
|
||||||
PkIface = "org.freedesktop.PackageKit"
|
PkIface = "org.freedesktop.PackageKit"
|
||||||
PkIfaceTransaction = PkIface + ".Transaction"
|
PkIfaceTransaction = PkIface + ".Transaction"
|
||||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -57,6 +57,7 @@ var (
|
|||||||
// TODO: add more values
|
// TODO: add more values
|
||||||
// noarch
|
// noarch
|
||||||
"noarch": "ANY", // special value "ANY" (noarch as seen in Fedora)
|
"noarch": "ANY", // special value "ANY" (noarch as seen in Fedora)
|
||||||
|
"any": "ANY", // special value "ANY" ('any' as seen in ArchLinux)
|
||||||
"all": "ANY", // special value "ANY" ('all' as seen in Debian)
|
"all": "ANY", // special value "ANY" ('all' as seen in Debian)
|
||||||
// fedora
|
// fedora
|
||||||
"x86_64": "amd64",
|
"x86_64": "amd64",
|
||||||
@@ -74,81 +75,84 @@ var (
|
|||||||
//type enum_filter uint64
|
//type enum_filter uint64
|
||||||
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
|
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
|
||||||
const ( //static const PkEnumMatch enum_filter[]
|
const ( //static const PkEnumMatch enum_filter[]
|
||||||
PK_FILTER_ENUM_UNKNOWN uint64 = 1 << iota // "unknown"
|
PkFilterEnumUnknown uint64 = 1 << iota // "unknown"
|
||||||
PK_FILTER_ENUM_NONE // "none"
|
PkFilterEnumNone // "none"
|
||||||
PK_FILTER_ENUM_INSTALLED // "installed"
|
PkFilterEnumInstalled // "installed"
|
||||||
PK_FILTER_ENUM_NOT_INSTALLED // "~installed"
|
PkFilterEnumNotInstalled // "~installed"
|
||||||
PK_FILTER_ENUM_DEVELOPMENT // "devel"
|
PkFilterEnumDevelopment // "devel"
|
||||||
PK_FILTER_ENUM_NOT_DEVELOPMENT // "~devel"
|
PkFilterEnumNotDevelopment // "~devel"
|
||||||
PK_FILTER_ENUM_GUI // "gui"
|
PkFilterEnumGui // "gui"
|
||||||
PK_FILTER_ENUM_NOT_GUI // "~gui"
|
PkFilterEnumNotGui // "~gui"
|
||||||
PK_FILTER_ENUM_FREE // "free"
|
PkFilterEnumFree // "free"
|
||||||
PK_FILTER_ENUM_NOT_FREE // "~free"
|
PkFilterEnumNotFree // "~free"
|
||||||
PK_FILTER_ENUM_VISIBLE // "visible"
|
PkFilterEnumVisible // "visible"
|
||||||
PK_FILTER_ENUM_NOT_VISIBLE // "~visible"
|
PkFilterEnumNotVisible // "~visible"
|
||||||
PK_FILTER_ENUM_SUPPORTED // "supported"
|
PkFilterEnumSupported // "supported"
|
||||||
PK_FILTER_ENUM_NOT_SUPPORTED // "~supported"
|
PkFilterEnumNotSupported // "~supported"
|
||||||
PK_FILTER_ENUM_BASENAME // "basename"
|
PkFilterEnumBasename // "basename"
|
||||||
PK_FILTER_ENUM_NOT_BASENAME // "~basename"
|
PkFilterEnumNotBasename // "~basename"
|
||||||
PK_FILTER_ENUM_NEWEST // "newest"
|
PkFilterEnumNewest // "newest"
|
||||||
PK_FILTER_ENUM_NOT_NEWEST // "~newest"
|
PkFilterEnumNotNewest // "~newest"
|
||||||
PK_FILTER_ENUM_ARCH // "arch"
|
PkFilterEnumArch // "arch"
|
||||||
PK_FILTER_ENUM_NOT_ARCH // "~arch"
|
PkFilterEnumNotArch // "~arch"
|
||||||
PK_FILTER_ENUM_SOURCE // "source"
|
PkFilterEnumSource // "source"
|
||||||
PK_FILTER_ENUM_NOT_SOURCE // "~source"
|
PkFilterEnumNotSource // "~source"
|
||||||
PK_FILTER_ENUM_COLLECTIONS // "collections"
|
PkFilterEnumCollections // "collections"
|
||||||
PK_FILTER_ENUM_NOT_COLLECTIONS // "~collections"
|
PkFilterEnumNotCollections // "~collections"
|
||||||
PK_FILTER_ENUM_APPLICATION // "application"
|
PkFilterEnumApplication // "application"
|
||||||
PK_FILTER_ENUM_NOT_APPLICATION // "~application"
|
PkFilterEnumNotApplication // "~application"
|
||||||
PK_FILTER_ENUM_DOWNLOADED // "downloaded"
|
PkFilterEnumDownloaded // "downloaded"
|
||||||
PK_FILTER_ENUM_NOT_DOWNLOADED // "~downloaded"
|
PkFilterEnumNotDownloaded // "~downloaded"
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants from packagekit c library.
|
// constants from packagekit c library.
|
||||||
const ( //static const PkEnumMatch enum_transaction_flag[]
|
const ( //static const PkEnumMatch enum_transaction_flag[]
|
||||||
PK_TRANSACTION_FLAG_ENUM_NONE uint64 = 1 << iota // "none"
|
PkTransactionFlagEnumNone uint64 = 1 << iota // "none"
|
||||||
PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED // "only-trusted"
|
PkTransactionFlagEnumOnlyTrusted // "only-trusted"
|
||||||
PK_TRANSACTION_FLAG_ENUM_SIMULATE // "simulate"
|
PkTransactionFlagEnumSimulate // "simulate"
|
||||||
PK_TRANSACTION_FLAG_ENUM_ONLY_DOWNLOAD // "only-download"
|
PkTransactionFlagEnumOnlyDownload // "only-download"
|
||||||
PK_TRANSACTION_FLAG_ENUM_ALLOW_REINSTALL // "allow-reinstall"
|
PkTransactionFlagEnumAllowReinstall // "allow-reinstall"
|
||||||
PK_TRANSACTION_FLAG_ENUM_JUST_REINSTALL // "just-reinstall"
|
PkTransactionFlagEnumJustReinstall // "just-reinstall"
|
||||||
PK_TRANSACTION_FLAG_ENUM_ALLOW_DOWNGRADE // "allow-downgrade"
|
PkTransactionFlagEnumAllowDowngrade // "allow-downgrade"
|
||||||
)
|
)
|
||||||
|
|
||||||
// constants from packagekit c library.
|
// constants from packagekit c library.
|
||||||
const ( //typedef enum
|
const ( //typedef enum
|
||||||
PK_INFO_ENUM_UNKNOWN uint64 = 1 << iota
|
PkInfoEnumUnknown uint64 = 1 << iota
|
||||||
PK_INFO_ENUM_INSTALLED
|
PkInfoEnumInstalled
|
||||||
PK_INFO_ENUM_AVAILABLE
|
PkInfoEnumAvailable
|
||||||
PK_INFO_ENUM_LOW
|
PkInfoEnumLow
|
||||||
PK_INFO_ENUM_ENHANCEMENT
|
PkInfoEnumEnhancement
|
||||||
PK_INFO_ENUM_NORMAL
|
PkInfoEnumNormal
|
||||||
PK_INFO_ENUM_BUGFIX
|
PkInfoEnumBugfix
|
||||||
PK_INFO_ENUM_IMPORTANT
|
PkInfoEnumImportant
|
||||||
PK_INFO_ENUM_SECURITY
|
PkInfoEnumSecurity
|
||||||
PK_INFO_ENUM_BLOCKED
|
PkInfoEnumBlocked
|
||||||
PK_INFO_ENUM_DOWNLOADING
|
PkInfoEnumDownloading
|
||||||
PK_INFO_ENUM_UPDATING
|
PkInfoEnumUpdating
|
||||||
PK_INFO_ENUM_INSTALLING
|
PkInfoEnumInstalling
|
||||||
PK_INFO_ENUM_REMOVING
|
PkInfoEnumRemoving
|
||||||
PK_INFO_ENUM_CLEANUP
|
PkInfoEnumCleanup
|
||||||
PK_INFO_ENUM_OBSOLETING
|
PkInfoEnumObsoleting
|
||||||
PK_INFO_ENUM_COLLECTION_INSTALLED
|
PkInfoEnumCollectionInstalled
|
||||||
PK_INFO_ENUM_COLLECTION_AVAILABLE
|
PkInfoEnumCollectionAvailable
|
||||||
PK_INFO_ENUM_FINISHED
|
PkInfoEnumFinished
|
||||||
PK_INFO_ENUM_REINSTALLING
|
PkInfoEnumReinstalling
|
||||||
PK_INFO_ENUM_DOWNGRADING
|
PkInfoEnumDowngrading
|
||||||
PK_INFO_ENUM_PREPARING
|
PkInfoEnumPreparing
|
||||||
PK_INFO_ENUM_DECOMPRESSING
|
PkInfoEnumDecompressing
|
||||||
PK_INFO_ENUM_UNTRUSTED
|
PkInfoEnumUntrusted
|
||||||
PK_INFO_ENUM_TRUSTED
|
PkInfoEnumTrusted
|
||||||
PK_INFO_ENUM_UNAVAILABLE
|
PkInfoEnumUnavailable
|
||||||
PK_INFO_ENUM_LAST
|
PkInfoEnumLast
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn is a wrapper struct so we can pass bus connection around in the struct.
|
// Conn is a wrapper struct so we can pass bus connection around in the struct.
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
conn *dbus.Conn
|
conn *dbus.Conn
|
||||||
|
|
||||||
|
Debug bool
|
||||||
|
Logf func(format string, v ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
|
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
|
||||||
@@ -173,58 +177,75 @@ func NewBus() *Conn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBus gets the dbus connection object.
|
// GetBus gets the dbus connection object.
|
||||||
func (bus *Conn) GetBus() *dbus.Conn {
|
func (obj *Conn) GetBus() *dbus.Conn {
|
||||||
return bus.conn
|
return obj.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the dbus connection object.
|
// Close closes the dbus connection object.
|
||||||
func (bus *Conn) Close() error {
|
func (obj *Conn) Close() error {
|
||||||
return bus.conn.Close()
|
return obj.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// internal helper to add signal matches to the bus, should only be called once
|
// internal helper to add signal matches to the bus, should only be called once
|
||||||
func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error {
|
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals)
|
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
|
||||||
}
|
}
|
||||||
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
|
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
|
||||||
var call *dbus.Call
|
bus := obj.GetBus().BusObject()
|
||||||
|
var argsList []string
|
||||||
|
// cleanup function should be called when done or when AddMatch errors
|
||||||
|
removeSignals := func() error {
|
||||||
|
var errList error
|
||||||
|
for i := len(argsList) - 1; i >= 0; i-- { // last in first out
|
||||||
|
if call := bus.Call(engineUtil.DBusRemoveMatch, 0, argsList[i]); call.Err != nil {
|
||||||
|
errList = multierr.Append(errList, call.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errList
|
||||||
|
}
|
||||||
// TODO: if we make this call many times, we seem to receive signals
|
// TODO: if we make this call many times, we seem to receive signals
|
||||||
// that many times... Maybe this should be an object singleton?
|
// that many times... Maybe this should be an object singleton?
|
||||||
obj := bus.GetBus().BusObject()
|
var call *dbus.Call
|
||||||
pathStr := fmt.Sprintf("%s", path)
|
pathStr := fmt.Sprintf("%s", path)
|
||||||
if len(signals) == 0 {
|
if len(signals) == 0 {
|
||||||
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"'")
|
args := fmt.Sprintf("type='signal', path='%s', interface='%s'", pathStr, iface)
|
||||||
|
argsList = append(argsList, args)
|
||||||
|
call = bus.Call(engineUtil.DBusAddMatch, 0, args)
|
||||||
} else {
|
} else {
|
||||||
for _, signal := range signals {
|
for _, signal := range signals {
|
||||||
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'")
|
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s'", pathStr, iface, signal)
|
||||||
if call.Err != nil {
|
argsList = append(argsList, args)
|
||||||
break
|
if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||||
|
break // fail if any one fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
defer removeSignals() // ignore the error
|
||||||
|
return nil, call.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// The caller has to make sure that ch is sufficiently buffered; if a
|
// The caller has to make sure that ch is sufficiently buffered; if a
|
||||||
// message arrives when a write to c is not possible, it is discarded!
|
// message arrives when a write to c is not possible, it is discarded!
|
||||||
// This can be disastrous if we're waiting for a "Finished" signal!
|
// This can be disastrous if we're waiting for a "Finished" signal!
|
||||||
bus.GetBus().Signal(ch)
|
obj.GetBus().Signal(ch)
|
||||||
return nil
|
return removeSignals, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WatchChanges gets a signal anytime an event happens.
|
// WatchChanges gets a signal anytime an event happens.
|
||||||
func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
func (obj *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize)
|
ch := make(chan *dbus.Signal, PkBufferSize)
|
||||||
// NOTE: the TransactionListChanged signal fires much more frequently,
|
// NOTE: the TransactionListChanged signal fires much more frequently,
|
||||||
// but with much less specificity. If we're missing events, report the
|
// but with much less specificity. If we're missing events, report the
|
||||||
// issue upstream! The UpdatesChanged signal is what hughsie suggested
|
// issue upstream! The UpdatesChanged signal is what hughsie suggested
|
||||||
var signal = "UpdatesChanged"
|
var signal = "UpdatesChanged"
|
||||||
err := bus.matchSignal(ch, PkPath, PkIface, []string{signal})
|
removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if PARANOID { // TODO: this filtering might not be necessary anymore...
|
defer removeSignals() // ignore the error
|
||||||
|
if Paranoid { // TODO: this filtering might not be necessary anymore...
|
||||||
// try to handle the filtering inside this function!
|
// try to handle the filtering inside this function!
|
||||||
rch := make(chan *dbus.Signal)
|
rch := make(chan *dbus.Signal)
|
||||||
go func() {
|
go func() {
|
||||||
@@ -236,13 +257,13 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
|||||||
// zero value immediately": if i get nil here,
|
// zero value immediately": if i get nil here,
|
||||||
// it means the channel was closed by someone!!
|
// it means the channel was closed by someone!!
|
||||||
if event == nil { // shared bus issue?
|
if event == nil { // shared bus issue?
|
||||||
log.Println("PackageKit: Hrm, channel was closed!")
|
obj.Logf("Hrm, channel was closed!")
|
||||||
break loop // TODO: continue?
|
break loop // TODO: continue?
|
||||||
}
|
}
|
||||||
// i think this was caused by using the shared
|
// i think this was caused by using the shared
|
||||||
// bus, but we might as well leave it in for now
|
// bus, but we might as well leave it in for now
|
||||||
if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) {
|
if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) {
|
||||||
log.Printf("PackageKit: Woops: Event: %+v", event)
|
obj.Logf("Woops: Event: %+v", event)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rch <- event // forward...
|
rch <- event // forward...
|
||||||
@@ -256,41 +277,45 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateTransaction creates and returns a transaction path.
|
// CreateTransaction creates and returns a transaction path.
|
||||||
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Println("PackageKit: CreateTransaction()")
|
obj.Logf("CreateTransaction()")
|
||||||
}
|
}
|
||||||
var interfacePath dbus.ObjectPath
|
var interfacePath dbus.ObjectPath
|
||||||
obj := bus.GetBus().Object(PkIface, PkPath)
|
bus := obj.GetBus().Object(PkIface, PkPath)
|
||||||
call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
||||||
if call != nil {
|
if call != nil {
|
||||||
return "", call
|
return "", call
|
||||||
}
|
}
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath)
|
obj.Logf("CreateTransaction(): %v", interfacePath)
|
||||||
}
|
}
|
||||||
return interfacePath, nil
|
return interfacePath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvePackages runs the PackageKit Resolve method and returns the result.
|
// ResolvePackages runs the PackageKit Resolve method and returns the result.
|
||||||
func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
|
func (obj *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
|
||||||
packageIDs := []string{}
|
packageIDs := []string{}
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}, err
|
return []string{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add signal matches for Package and Finished which will always be last
|
// add signal matches for Package and Finished which will always be last
|
||||||
var signals = []string{"Package", "Finished", "Error", "Destroy"}
|
var signals = []string{"Package", "Finished", "Error", "Destroy"}
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
if PK_DEBUG {
|
if err != nil {
|
||||||
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath)
|
return nil, err
|
||||||
}
|
}
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
defer removeSignals()
|
||||||
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
|
if obj.Debug {
|
||||||
if PK_DEBUG {
|
obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath)
|
||||||
log.Println("PackageKit: ResolvePackages(): Call: Success!")
|
}
|
||||||
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
|
call := bus.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
|
||||||
|
if obj.Debug {
|
||||||
|
obj.Logf("ResolvePackages(): Call: Success!")
|
||||||
}
|
}
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return []string{}, call.Err
|
return []string{}, call.Err
|
||||||
@@ -300,11 +325,11 @@ loop:
|
|||||||
// FIXME: add a timeout option to error in case signals are dropped!
|
// FIXME: add a timeout option to error in case signals are dropped!
|
||||||
select {
|
select {
|
||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal)
|
obj.Logf("ResolvePackages(): Signal: %+v", signal)
|
||||||
}
|
}
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,10 +362,10 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsInstalledList queries a list of packages to see if they are installed.
|
// IsInstalledList queries a list of packages to see if they are installed.
|
||||||
func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
||||||
var filter uint64 // initializes at the "zero" value of 0
|
var filter uint64 // initializes at the "zero" value of 0
|
||||||
filter += PK_FILTER_ENUM_ARCH // always search in our arch
|
filter += PkFilterEnumArch // always search in our arch
|
||||||
packageIDs, e := bus.ResolvePackages(packages, filter)
|
packageIDs, e := obj.ResolvePackages(packages, filter)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, fmt.Errorf("ResolvePackages error: %v", e)
|
return nil, fmt.Errorf("ResolvePackages error: %v", e)
|
||||||
}
|
}
|
||||||
@@ -375,8 +400,8 @@ func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
|
|||||||
|
|
||||||
// IsInstalled returns if a package is installed.
|
// IsInstalled returns if a package is installed.
|
||||||
// TODO: this could be optimized by making the resolve call directly
|
// TODO: this could be optimized by making the resolve call directly
|
||||||
func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
func (obj *Conn) IsInstalled(pkg string) (bool, error) {
|
||||||
p, e := bus.IsInstalledList([]string{pkg})
|
p, e := obj.IsInstalledList([]string{pkg})
|
||||||
if len(p) != 1 {
|
if len(p) != 1 {
|
||||||
return false, e
|
return false, e
|
||||||
}
|
}
|
||||||
@@ -384,19 +409,27 @@ func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// InstallPackages installs a list of packages by packageID.
|
// InstallPackages installs a list of packages by packageID.
|
||||||
func (bus *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
|
func (obj *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
|
||||||
|
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer removeSignals()
|
||||||
|
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
call := obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false)
|
||||||
|
if call.Err != nil {
|
||||||
|
return call.Err
|
||||||
|
}
|
||||||
|
call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
@@ -407,7 +440,7 @@ loop:
|
|||||||
select {
|
select {
|
||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,30 +460,34 @@ loop:
|
|||||||
}
|
}
|
||||||
case <-util.TimeAfterOrBlock(timeout):
|
case <-util.TimeAfterOrBlock(timeout):
|
||||||
if finished {
|
if finished {
|
||||||
log.Println("PackageKit: Timeout: InstallPackages: Waiting for 'Destroy'")
|
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
|
||||||
return nil // got tired of waiting for Destroy
|
return nil // got tired of waiting for Destroy
|
||||||
}
|
}
|
||||||
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %v", strings.Join(packageIDs, ", "))
|
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %s", strings.Join(packageIDs, ", "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemovePackages removes a list of packages by packageID.
|
// RemovePackages removes a list of packages by packageID.
|
||||||
func (bus *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
||||||
|
|
||||||
var allowDeps = true // TODO: configurable
|
var allowDeps = true // TODO: configurable
|
||||||
var autoremove = false // unsupported on GNU/Linux
|
var autoremove = false // unsupported on GNU/Linux
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer removeSignals()
|
||||||
|
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
call := obj.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
@@ -460,7 +497,7 @@ loop:
|
|||||||
select {
|
select {
|
||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,18 +521,22 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdatePackages updates a list of packages to versions that are specified.
|
// UpdatePackages updates a list of packages to versions that are specified.
|
||||||
func (bus *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
|
func (obj *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction()
|
interfacePath, err := obj.CreateTransaction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer removeSignals()
|
||||||
|
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
call := obj.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return call.Err
|
return call.Err
|
||||||
}
|
}
|
||||||
@@ -505,7 +546,7 @@ loop:
|
|||||||
select {
|
select {
|
||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,20 +568,24 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
|
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
|
||||||
func (bus *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
|
||||||
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
|
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
|
||||||
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction()
|
interfacePath, err := obj.CreateTransaction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer removeSignals()
|
||||||
|
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
call := obj.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
err = call.Err
|
err = call.Err
|
||||||
return
|
return
|
||||||
@@ -553,7 +598,7 @@ loop:
|
|||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
|
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,22 +637,26 @@ loop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
|
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
|
||||||
func (bus *Conn) GetUpdates(filter uint64) ([]string, error) {
|
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Println("PackageKit: GetUpdates()")
|
obj.Logf("GetUpdates()")
|
||||||
}
|
}
|
||||||
packageIDs := []string{}
|
packageIDs := []string{}
|
||||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||||
interfacePath, err := bus.CreateTransaction()
|
interfacePath, err := obj.CreateTransaction()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ?
|
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ?
|
||||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer removeSignals()
|
||||||
|
|
||||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||||
call := obj.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
||||||
if call.Err != nil {
|
if call.Err != nil {
|
||||||
return nil, call.Err
|
return nil, call.Err
|
||||||
}
|
}
|
||||||
@@ -617,7 +666,7 @@ loop:
|
|||||||
select {
|
select {
|
||||||
case signal := <-ch:
|
case signal := <-ch:
|
||||||
if signal.Path != interfacePath {
|
if signal.Path != interfacePath {
|
||||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||||
continue loop
|
continue loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,7 +705,7 @@ loop:
|
|||||||
// outside mgmt. The packageMap input has the package names as keys and
|
// outside mgmt. The packageMap input has the package names as keys and
|
||||||
// requested states as values. These states can be: installed, uninstalled,
|
// requested states as values. These states can be: installed, uninstalled,
|
||||||
// newest or a requested version str.
|
// newest or a requested version str.
|
||||||
func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
|
func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
|
||||||
count := 0
|
count := 0
|
||||||
packages := make([]string, len(packageMap))
|
packages := make([]string, len(packageMap))
|
||||||
for k := range packageMap { // lol, golang has no hash.keys() function!
|
for k := range packageMap { // lol, golang has no hash.keys() function!
|
||||||
@@ -664,14 +713,14 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(filter&PK_FILTER_ENUM_ARCH == PK_FILTER_ENUM_ARCH) {
|
if !(filter&PkFilterEnumArch == PkFilterEnumArch) {
|
||||||
filter += PK_FILTER_ENUM_ARCH // always search in our arch
|
filter += PkFilterEnumArch // always search in our arch
|
||||||
}
|
}
|
||||||
|
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Printf("PackageKit: PackagesToPackageIDs(): %v", strings.Join(packages, ", "))
|
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
|
||||||
}
|
}
|
||||||
resolved, e := bus.ResolvePackages(packages, filter)
|
resolved, e := obj.ResolvePackages(packages, filter)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, fmt.Errorf("Resolve error: %v", e)
|
return nil, fmt.Errorf("Resolve error: %v", e)
|
||||||
}
|
}
|
||||||
@@ -688,13 +737,16 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
|
|
||||||
for _, packageID := range resolved {
|
for _, packageID := range resolved {
|
||||||
index = -1
|
index = -1
|
||||||
//log.Printf("* %v", packageID)
|
//obj.Logf("* %v", packageID)
|
||||||
// format is: name;version;arch;data
|
// format is: name;version;arch;data
|
||||||
s := strings.Split(packageID, ";")
|
s := strings.Split(packageID, ";")
|
||||||
//if len(s) != 4 { continue } // this would be a bug!
|
//if len(s) != 4 { continue } // this would be a bug!
|
||||||
pkg, ver, arch, data := s[0], s[1], s[2], s[3]
|
pkg, ver, arch, data := s[0], s[1], s[2], s[3]
|
||||||
// we might need to allow some of this, eg: i386 .deb on amd64
|
// we might need to allow some of this, eg: i386 .deb on amd64
|
||||||
if !IsMyArch(arch) {
|
b, err := IsMyArch(arch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "arch error")
|
||||||
|
} else if !b {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,12 +796,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
// to be done, and if so, anything that needs updating isn't newest!
|
// to be done, and if so, anything that needs updating isn't newest!
|
||||||
// if something isn't installed, we can't verify it with this method
|
// if something isn't installed, we can't verify it with this method
|
||||||
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
||||||
updates, e := bus.GetUpdates(filter)
|
updates, e := obj.GetUpdates(filter)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, fmt.Errorf("Updates error: %v", e)
|
return nil, fmt.Errorf("Updates error: %v", e)
|
||||||
}
|
}
|
||||||
for _, packageID := range updates {
|
for _, packageID := range updates {
|
||||||
//log.Printf("* %v", packageID)
|
//obj.Logf("* %v", packageID)
|
||||||
// format is: name;version;arch;data
|
// format is: name;version;arch;data
|
||||||
s := strings.Split(packageID, ";")
|
s := strings.Split(packageID, ";")
|
||||||
//if len(s) != 4 { continue } // this would be a bug!
|
//if len(s) != 4 { continue } // this would be a bug!
|
||||||
@@ -771,7 +823,7 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
// this check is for packages that need to verify their "newest" status
|
// this check is for packages that need to verify their "newest" status
|
||||||
// we need to know this so we can install the correct newest packageID!
|
// we need to know this so we can install the correct newest packageID!
|
||||||
recursion := make(map[string]*PkPackageIDActionData)
|
recursion := make(map[string]*PkPackageIDActionData)
|
||||||
if !(filter&PK_FILTER_ENUM_NEWEST == PK_FILTER_ENUM_NEWEST) {
|
if !(filter&PkFilterEnumNewest == PkFilterEnumNewest) {
|
||||||
checkPackages := []string{}
|
checkPackages := []string{}
|
||||||
filteredPackageMap := make(map[string]string)
|
filteredPackageMap := make(map[string]string)
|
||||||
for index, pkg := range packages {
|
for index, pkg := range packages {
|
||||||
@@ -788,13 +840,13 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
}
|
}
|
||||||
|
|
||||||
// we _could_ do a second resolve and then parse like this...
|
// we _could_ do a second resolve and then parse like this...
|
||||||
//resolved, e := bus.ResolvePackages(..., filter+PK_FILTER_ENUM_NEWEST)
|
//resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest)
|
||||||
// but that's basically what recursion here could do too!
|
// but that's basically what recursion here could do too!
|
||||||
if len(checkPackages) > 0 {
|
if len(checkPackages) > 0 {
|
||||||
if PK_DEBUG {
|
if obj.Debug {
|
||||||
log.Printf("PackageKit: PackagesToPackageIDs(): Recurse: %v", strings.Join(checkPackages, ", "))
|
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
|
||||||
}
|
}
|
||||||
recursion, e = bus.PackagesToPackageIDs(filteredPackageMap, filter+PK_FILTER_ENUM_NEWEST)
|
recursion, e = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, fmt.Errorf("Recursion error: %v", e)
|
return nil, fmt.Errorf("Recursion error: %v", e)
|
||||||
}
|
}
|
||||||
@@ -830,12 +882,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
|
|||||||
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
|
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
|
||||||
result := []string{}
|
result := []string{}
|
||||||
for _, k := range packages {
|
for _, k := range packages {
|
||||||
obj, ok := m[k] // lookup single package
|
p, ok := m[k] // lookup single package
|
||||||
// package doesn't exist, this is an error!
|
// package doesn't exist, this is an error!
|
||||||
if !ok || !obj.Found || obj.PackageID == "" {
|
if !ok || !p.Found || p.PackageID == "" {
|
||||||
return nil, fmt.Errorf("Can't find package named '%s'.", k)
|
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||||
}
|
}
|
||||||
result = append(result, obj.PackageID)
|
result = append(result, p.PackageID)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -845,18 +897,18 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
|||||||
result = make(map[string]bool)
|
result = make(map[string]bool)
|
||||||
pkgs := []string{} // bad pkgs that don't have a bool state
|
pkgs := []string{} // bad pkgs that don't have a bool state
|
||||||
for _, k := range packages {
|
for _, k := range packages {
|
||||||
obj, ok := m[k] // lookup single package
|
p, ok := m[k] // lookup single package
|
||||||
// package doesn't exist, this is an error!
|
// package doesn't exist, this is an error!
|
||||||
if !ok || !obj.Found {
|
if !ok || !p.Found {
|
||||||
return nil, fmt.Errorf("Can't find package named '%s'.", k)
|
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||||
}
|
}
|
||||||
var b bool
|
var b bool
|
||||||
if state == "installed" {
|
if state == "installed" {
|
||||||
b = obj.Installed
|
b = p.Installed
|
||||||
} else if state == "uninstalled" {
|
} else if state == "uninstalled" {
|
||||||
b = !obj.Installed
|
b = !p.Installed
|
||||||
} else if state == "newest" {
|
} else if state == "newest" {
|
||||||
b = obj.Newest
|
b = p.Newest
|
||||||
} else {
|
} else {
|
||||||
// we can't filter "version" state in this function
|
// we can't filter "version" state in this function
|
||||||
pkgs = append(pkgs, k)
|
pkgs = append(pkgs, k)
|
||||||
@@ -865,7 +917,7 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
|||||||
result[k] = b // save
|
result[k] = b // save
|
||||||
}
|
}
|
||||||
if len(pkgs) > 0 {
|
if len(pkgs) > 0 {
|
||||||
err = fmt.Errorf("Can't filter non-boolean state on: %v!", strings.Join(pkgs, ","))
|
err = fmt.Errorf("can't filter non-boolean state on: %s", strings.Join(pkgs, ","))
|
||||||
}
|
}
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -874,19 +926,19 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
|||||||
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
|
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
|
||||||
result = []string{}
|
result = []string{}
|
||||||
for _, k := range packages {
|
for _, k := range packages {
|
||||||
obj, ok := m[k] // lookup single package
|
p, ok := m[k] // lookup single package
|
||||||
// package doesn't exist, this is an error!
|
// package doesn't exist, this is an error!
|
||||||
if !ok || !obj.Found {
|
if !ok || !p.Found {
|
||||||
return nil, fmt.Errorf("Can't find package named '%s'.", k)
|
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||||
}
|
}
|
||||||
b := false
|
b := false
|
||||||
if state == "installed" && obj.Installed {
|
if state == "installed" && p.Installed {
|
||||||
b = true
|
b = true
|
||||||
} else if state == "uninstalled" && !obj.Installed {
|
} else if state == "uninstalled" && !p.Installed {
|
||||||
b = true
|
b = true
|
||||||
} else if state == "newest" && obj.Newest {
|
} else if state == "newest" && p.Newest {
|
||||||
b = true
|
b = true
|
||||||
} else if state == obj.Version {
|
} else if state == p.Version {
|
||||||
b = true
|
b = true
|
||||||
}
|
}
|
||||||
if b {
|
if b {
|
||||||
@@ -913,14 +965,14 @@ func FmtTransactionMethod(method string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsMyArch determines if a PackageKit architecture matches the current os arch.
|
// IsMyArch determines if a PackageKit architecture matches the current os arch.
|
||||||
func IsMyArch(arch string) bool {
|
func IsMyArch(arch string) (bool, error) {
|
||||||
goarch, ok := PkArchMap[arch]
|
goarch, ok := PkArchMap[arch]
|
||||||
if !ok {
|
if !ok {
|
||||||
// if you get this error, please update the PkArchMap const
|
// if you get this error, please update the PkArchMap const
|
||||||
log.Fatalf("PackageKit: Arch '%v', not found!", arch)
|
return false, fmt.Errorf("arch '%s', not found", arch)
|
||||||
}
|
}
|
||||||
if goarch == "ANY" { // special value that corresponds to noarch
|
if goarch == "ANY" { // special value that corresponds to noarch
|
||||||
return true
|
return true, nil
|
||||||
}
|
}
|
||||||
return goarch == runtime.GOARCH
|
return goarch == runtime.GOARCH, nil
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,40 @@
|
|||||||
// Mgmt
|
// Mgmt
|
||||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
// Written by James Shubin <james@shubin.ca> 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
|
// 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
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
// (at your option) any later version.
|
// (at your option) any later version.
|
||||||
//
|
//
|
||||||
// This program is distributed in the hope that it will be useful,
|
// This program is distributed in the hope that it will be useful,
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package resources
|
package resources
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/gob"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/purpleidea/mgmt/event"
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
"github.com/purpleidea/mgmt/recwatch"
|
"github.com/purpleidea/mgmt/recwatch"
|
||||||
|
|
||||||
errwrap "github.com/pkg/errors"
|
errwrap "github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gob.Register(&PasswordRes{})
|
engine.RegisterResource("password", func() engine.Res { return &PasswordRes{} })
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,45 +44,53 @@ const (
|
|||||||
|
|
||||||
// PasswordRes is a no-op resource that returns a random password string.
|
// PasswordRes is a no-op resource that returns a random password string.
|
||||||
type PasswordRes struct {
|
type PasswordRes struct {
|
||||||
BaseRes `yaml:",inline"`
|
traits.Base // add the base methods without re-implementation
|
||||||
|
// TODO: it could be useful to group our tokens into a single write, and
|
||||||
|
// as a result, we save inotify watches too!
|
||||||
|
//traits.Groupable // TODO: this is doable, but probably not very useful
|
||||||
|
traits.Refreshable
|
||||||
|
traits.Sendable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
// FIXME: is uint16 too big?
|
// FIXME: is uint16 too big?
|
||||||
Length uint16 `yaml:"length"` // number of characters to return
|
Length uint16 `yaml:"length"` // number of characters to return
|
||||||
Saved bool // this caches the password in the clear locally
|
Saved bool // this caches the password in the clear locally
|
||||||
CheckRecovery bool // recovery from integrity checks by re-generating
|
CheckRecovery bool // recovery from integrity checks by re-generating
|
||||||
Password *string // the generated password, read only, do not set!
|
|
||||||
|
|
||||||
path string // the path to local storage
|
path string // the path to local storage
|
||||||
recWatcher *recwatch.RecWatcher
|
recWatcher *recwatch.RecWatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPasswordRes is a constructor for this resource. It also calls Init() for you.
|
// Default returns some sensible defaults for this resource.
|
||||||
func NewPasswordRes(name string, length uint16) (*PasswordRes, error) {
|
func (obj *PasswordRes) Default() engine.Res {
|
||||||
obj := &PasswordRes{
|
return &PasswordRes{
|
||||||
BaseRes: BaseRes{
|
Length: 64, // safe default
|
||||||
Name: name,
|
|
||||||
},
|
|
||||||
Length: length,
|
|
||||||
}
|
}
|
||||||
return obj, obj.Init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init generates a new password for this resource if one was not provided. It
|
// Validate if the params passed in are valid data.
|
||||||
// will save this into a local file. It will load it back in from previous runs.
|
func (obj *PasswordRes) Validate() error {
|
||||||
func (obj *PasswordRes) Init() error {
|
return nil
|
||||||
obj.BaseRes.kind = "Password" // must be set before using VarDir
|
}
|
||||||
|
|
||||||
dir, err := obj.VarDir("")
|
// Init runs some startup code for this resource. It generates a new password
|
||||||
|
// for this resource if one was not provided. It will save this into a local
|
||||||
|
// file. It will load it back in from previous runs.
|
||||||
|
func (obj *PasswordRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
dir, err := obj.init.VarDir("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||||
}
|
}
|
||||||
obj.path = path.Join(dir, "password") // return a unique file
|
obj.path = path.Join(dir, "password") // return a unique file
|
||||||
|
|
||||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate if the params passed in are valid data.
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
// FIXME: where should this get called ?
|
func (obj *PasswordRes) Close() error {
|
||||||
func (obj *PasswordRes) Validate() error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,11 +154,11 @@ func (obj *PasswordRes) check(value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if !obj.Saved && length != 0 { // should have no stored password
|
if !obj.Saved && length != 0 { // should have no stored password
|
||||||
return fmt.Errorf("Expected empty token only!")
|
return fmt.Errorf("expected empty token only")
|
||||||
}
|
}
|
||||||
|
|
||||||
if length != obj.Length {
|
if length != obj.Length {
|
||||||
return fmt.Errorf("String length is not %d", obj.Length)
|
return fmt.Errorf("string length is not %d", obj.Length)
|
||||||
}
|
}
|
||||||
Loop:
|
Loop:
|
||||||
for i := uint16(0); i < length; i++ {
|
for i := uint16(0); i < length; i++ {
|
||||||
@@ -162,30 +168,13 @@ Loop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// we couldn't find that character, so error!
|
// we couldn't find that character, so error!
|
||||||
return fmt.Errorf("Invalid character `%s`", string(value[i]))
|
return fmt.Errorf("invalid character `%s`", string(value[i]))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch is the primary listener for this resource and it outputs events.
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
func (obj *PasswordRes) Watch(processChan chan event.Event) error {
|
func (obj *PasswordRes) Watch() error {
|
||||||
if obj.IsWatching() {
|
|
||||||
return nil // TODO: should this be an error?
|
|
||||||
}
|
|
||||||
obj.SetWatching(true)
|
|
||||||
defer obj.SetWatching(false)
|
|
||||||
cuid := obj.converger.Register()
|
|
||||||
defer cuid.Unregister()
|
|
||||||
|
|
||||||
var startup bool
|
|
||||||
Startup := func(block bool) <-chan time.Time {
|
|
||||||
if block {
|
|
||||||
return nil // blocks forever
|
|
||||||
//return make(chan time.Time) // blocks forever
|
|
||||||
}
|
|
||||||
return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
obj.recWatcher, err = recwatch.NewRecWatcher(obj.path, false)
|
obj.recWatcher, err = recwatch.NewRecWatcher(obj.path, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,45 +182,39 @@ func (obj *PasswordRes) Watch(processChan chan event.Event) error {
|
|||||||
}
|
}
|
||||||
defer obj.recWatcher.Close()
|
defer obj.recWatcher.Close()
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
var send = false // send event?
|
var send = false // send event?
|
||||||
var exit = false
|
|
||||||
for {
|
for {
|
||||||
obj.SetState(ResStateWatching) // reset
|
|
||||||
select {
|
select {
|
||||||
// NOTE: this part is very similar to the file resource code
|
// NOTE: this part is very similar to the file resource code
|
||||||
case event, ok := <-obj.recWatcher.Events():
|
case event, ok := <-obj.recWatcher.Events():
|
||||||
if !ok { // channel shutdown
|
if !ok { // channel shutdown
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
cuid.SetConverged(false)
|
|
||||||
if err := event.Error; err != nil {
|
if err := event.Error; err != nil {
|
||||||
return errwrap.Wrapf(err, "Unknown %s[%s] watcher error", obj.Kind(), obj.GetName())
|
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||||
}
|
}
|
||||||
send = true
|
send = true
|
||||||
obj.StateOK(false) // dirty
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
case event := <-obj.Events():
|
case event, ok := <-obj.init.Events:
|
||||||
cuid.SetConverged(false)
|
if !ok {
|
||||||
// we avoid sending events on unpause
|
return nil
|
||||||
if exit, send = obj.ReadEvent(&event); exit {
|
}
|
||||||
return nil // exit
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-cuid.ConvergedTimer():
|
|
||||||
cuid.SetConverged(true) // converged!
|
|
||||||
continue
|
|
||||||
|
|
||||||
case <-Startup(startup):
|
|
||||||
cuid.SetConverged(false)
|
|
||||||
send = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// do all our event sending all together to avoid duplicate msgs
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
if send {
|
if send {
|
||||||
startup = true // startup finished
|
|
||||||
send = false
|
send = false
|
||||||
if exit, err := obj.DoSend(processChan, ""); exit || err != nil {
|
if err := obj.init.Event(); err != nil {
|
||||||
return err // we exit or bubble up a NACK...
|
return err // exit if requested
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -239,11 +222,10 @@ func (obj *PasswordRes) Watch(processChan chan event.Event) error {
|
|||||||
|
|
||||||
// CheckApply method for Password resource. Does nothing, returns happy!
|
// CheckApply method for Password resource. Does nothing, returns happy!
|
||||||
func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||||
var refresh = obj.Refresh() // do we have a pending reload to apply?
|
var exists = true // does the file (aka the token) exist?
|
||||||
var exists = true // does the file (aka the token) exist?
|
var generate bool // do we need to generate a new password?
|
||||||
var generate bool // do we need to generate a new password?
|
var write bool // do we need to write out to disk?
|
||||||
var write bool // do we need to write out to disk?
|
|
||||||
|
|
||||||
password, err := obj.read() // password might be empty if just a token
|
password, err := obj.read() // password might be empty if just a token
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -258,7 +240,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
if !obj.CheckRecovery {
|
if !obj.CheckRecovery {
|
||||||
return false, errwrap.Wrapf(err, "check failed")
|
return false, errwrap.Wrapf(err, "check failed")
|
||||||
}
|
}
|
||||||
log.Printf("%s[%s]: Integrity check failed", obj.Kind(), obj.GetName())
|
obj.init.Logf("integrity check failed")
|
||||||
generate = true // okay to build a new one
|
generate = true // okay to build a new one
|
||||||
write = true // make sure to write over the old one
|
write = true // make sure to write over the old one
|
||||||
}
|
}
|
||||||
@@ -272,9 +254,9 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// stored password isn't consistent with memory
|
// stored password isn't consistent with memory
|
||||||
if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
//if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
||||||
write = true
|
// write = true
|
||||||
}
|
//}
|
||||||
|
|
||||||
if !refresh && exists && !generate && !write { // nothing to do, done!
|
if !refresh && exists && !generate && !write { // nothing to do, done!
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -292,13 +274,18 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
}
|
}
|
||||||
// generate the actual password
|
// generate the actual password
|
||||||
var err error
|
var err error
|
||||||
log.Printf("%s[%s]: Generating new password...", obj.Kind(), obj.GetName())
|
obj.init.Logf("generating new password...")
|
||||||
if password, err = obj.generate(); err != nil { // generate one!
|
if password, err = obj.generate(); err != nil { // generate one!
|
||||||
return false, errwrap.Wrapf(err, "could not generate password")
|
return false, errwrap.Wrapf(err, "could not generate password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
obj.Password = &password // save in memory
|
// send
|
||||||
|
if err := obj.init.Send(&PasswordSends{
|
||||||
|
Password: &password,
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
var output string // the string to write out
|
var output string // the string to write out
|
||||||
|
|
||||||
@@ -309,7 +296,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
output = password
|
output = password
|
||||||
}
|
}
|
||||||
// write either an empty token, or the password
|
// write either an empty token, or the password
|
||||||
log.Printf("%s[%s]: Writing password token...", obj.Kind(), obj.GetName())
|
obj.init.Logf("writing password token...")
|
||||||
if _, err := obj.write(output); err != nil {
|
if _, err := obj.write(output); err != nil {
|
||||||
return false, errwrap.Wrapf(err, "can't write to file")
|
return false, errwrap.Wrapf(err, "can't write to file")
|
||||||
}
|
}
|
||||||
@@ -318,64 +305,84 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
|||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasswordUID is the UID struct for PasswordRes.
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
type PasswordUID struct {
|
func (obj *PasswordRes) Cmp(r engine.Res) error {
|
||||||
BaseUID
|
if !obj.Compare(r) {
|
||||||
name string
|
return fmt.Errorf("did not compare")
|
||||||
}
|
}
|
||||||
|
|
||||||
// AutoEdges returns the AutoEdge interface. In this case no autoedges are used.
|
|
||||||
func (obj *PasswordRes) AutoEdges() AutoEdge {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUIDs includes all params to make a unique identification of this object.
|
// Compare two resources and return if they are equivalent.
|
||||||
// Most resources only return one, although some resources can return multiple.
|
func (obj *PasswordRes) Compare(r engine.Res) bool {
|
||||||
func (obj *PasswordRes) GetUIDs() []ResUID {
|
// we can only compare PasswordRes to others of the same resource kind
|
||||||
x := &PasswordUID{
|
res, ok := r.(*PasswordRes)
|
||||||
BaseUID: BaseUID{name: obj.GetName(), kind: obj.Kind()},
|
|
||||||
name: obj.Name,
|
|
||||||
}
|
|
||||||
return []ResUID{x}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupCmp returns whether two resources can be grouped together or not.
|
|
||||||
func (obj *PasswordRes) GroupCmp(r Res) bool {
|
|
||||||
_, ok := r.(*PasswordRes)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return false // TODO: this is doable, but probably not very useful
|
|
||||||
// TODO: it could be useful to group our tokens into a single write, and
|
|
||||||
// as a result, we save inotify watches too!
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare two resources and return if they are equivalent.
|
if obj.Length != res.Length {
|
||||||
func (obj *PasswordRes) Compare(res Res) bool {
|
|
||||||
switch res.(type) {
|
|
||||||
// we can only compare PasswordRes to others of the same resource
|
|
||||||
case *PasswordRes:
|
|
||||||
res := res.(*PasswordRes)
|
|
||||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if obj.Name != res.Name {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.Length != res.Length {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// TODO: we *could* optimize by allowing CheckApply to move from
|
|
||||||
// saved->!saved, by removing the file, but not likely worth it!
|
|
||||||
if obj.Saved != res.Saved {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if obj.CheckRecovery != res.CheckRecovery {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// TODO: we *could* optimize by allowing CheckApply to move from
|
||||||
|
// saved->!saved, by removing the file, but not likely worth it!
|
||||||
|
if obj.Saved != res.Saved {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if obj.CheckRecovery != res.CheckRecovery {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PasswordUID is the UID struct for PasswordRes.
|
||||||
|
type PasswordUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *PasswordRes) UIDs() []engine.ResUID {
|
||||||
|
x := &PasswordUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PasswordSends is the struct of data which is sent after a successful Apply.
|
||||||
|
type PasswordSends struct {
|
||||||
|
// Password is the generated password being sent.
|
||||||
|
Password *string
|
||||||
|
// Hashing is the algorithm used for this password. Empty is plain text.
|
||||||
|
Hashing string // TODO: implement me
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends represents the default struct of values we can send using Send/Recv.
|
||||||
|
func (obj *PasswordRes) Sends() interface{} {
|
||||||
|
return &PasswordSends{
|
||||||
|
Password: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes PasswordRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*PasswordRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to PasswordRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = PasswordRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
680
engine/resources/pkg.go
Normal file
680
engine/resources/pkg.go
Normal file
@@ -0,0 +1,680 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/resources/packagekit"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
"github.com/purpleidea/mgmt/util"
|
||||||
|
|
||||||
|
errwrap "github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("pkg", func() engine.Res { return &PkgRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// PkgStateInstalled is the string that represents that the package
|
||||||
|
// should be installed.
|
||||||
|
PkgStateInstalled = "installed"
|
||||||
|
|
||||||
|
// PkgStateUninstalled is the string that represents that the package
|
||||||
|
// should be uninstalled.
|
||||||
|
PkgStateUninstalled = "uninstalled"
|
||||||
|
|
||||||
|
// PkgStateNewest is the string that represents that the package should
|
||||||
|
// be installed in the newest available version.
|
||||||
|
PkgStateNewest = "newest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PkgRes is a package resource for packagekit.
|
||||||
|
type PkgRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Edgeable
|
||||||
|
traits.Groupable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
State string `yaml:"state"` // state: installed, uninstalled, newest, <version>
|
||||||
|
AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed?
|
||||||
|
AllowNonFree bool `yaml:"allownonfree"` // allow nonfree packages to be found?
|
||||||
|
AllowUnsupported bool `yaml:"allowunsupported"` // allow unsupported packages to be found?
|
||||||
|
//bus *packagekit.Conn // pk bus connection
|
||||||
|
fileList []string // FIXME: update if pkg changes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *PkgRes) Default() engine.Res {
|
||||||
|
return &PkgRes{
|
||||||
|
State: PkgStateInstalled, // i think this is preferable to "latest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the resource data structure was populated correctly.
|
||||||
|
func (obj *PkgRes) Validate() error {
|
||||||
|
if obj.State == "" {
|
||||||
|
return fmt.Errorf("state cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *PkgRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
if obj.fileList == nil {
|
||||||
|
if err := obj.populateFileList(); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error populating file list in init")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *PkgRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
// It uses the PackageKit UpdatesChanged signal to watch for changes.
|
||||||
|
// TODO: https://github.com/hughsie/PackageKit/issues/109
|
||||||
|
// TODO: https://github.com/hughsie/PackageKit/issues/110
|
||||||
|
func (obj *PkgRes) Watch() error {
|
||||||
|
bus := packagekit.NewBus()
|
||||||
|
if bus == nil {
|
||||||
|
return fmt.Errorf("can't connect to PackageKit bus")
|
||||||
|
}
|
||||||
|
defer bus.Close()
|
||||||
|
bus.Debug = obj.init.Debug
|
||||||
|
bus.Logf = func(format string, v ...interface{}) {
|
||||||
|
obj.init.Logf("packagekit: "+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
ch, err := bus.WatchChanges()
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error adding signal match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("%s: Watching...", obj.fmtNames(obj.getNames()))
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case event := <-ch:
|
||||||
|
// FIXME: ask packagekit for info on what packages changed
|
||||||
|
if obj.init.Debug {
|
||||||
|
obj.init.Logf("Event(%s): %s", event.Name, obj.fmtNames(obj.getNames()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// since the chan is buffered, remove any supplemental
|
||||||
|
// events since they would just be duplicates anyways!
|
||||||
|
for len(ch) > 0 { // we can detect pending count here!
|
||||||
|
<-ch // discard
|
||||||
|
}
|
||||||
|
|
||||||
|
send = true
|
||||||
|
obj.init.Dirty() // dirty
|
||||||
|
|
||||||
|
case event := <-obj.init.Events:
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get list of names when grouped or not
|
||||||
|
func (obj *PkgRes) getNames() []string {
|
||||||
|
if g := obj.GetGroup(); len(g) > 0 { // grouped elements
|
||||||
|
names := []string{obj.Name()}
|
||||||
|
for _, x := range g {
|
||||||
|
pkg, ok := x.(*PkgRes) // convert from Res
|
||||||
|
if ok {
|
||||||
|
names = append(names, pkg.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
return []string{obj.Name()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pretty print for header values
|
||||||
|
func (obj *PkgRes) fmtNames(names []string) string {
|
||||||
|
if len(obj.GetGroup()) > 0 { // grouped elements
|
||||||
|
return fmt.Sprintf("%s[autogroup:(%s)]", obj.Kind(), strings.Join(names, ","))
|
||||||
|
}
|
||||||
|
return obj.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *PkgRes) groupMappingHelper() map[string]string {
|
||||||
|
var result = make(map[string]string)
|
||||||
|
if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements
|
||||||
|
for _, x := range g {
|
||||||
|
pkg, ok := x.(*PkgRes) // convert from Res
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
|
||||||
|
}
|
||||||
|
result[pkg.Name()] = pkg.State
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) {
|
||||||
|
packageMap := obj.groupMappingHelper() // get the grouped values
|
||||||
|
packageMap[obj.Name()] = obj.State // key is pkg name, value is pkg state
|
||||||
|
var filter uint64 // initializes at the "zero" value of 0
|
||||||
|
filter += packagekit.PkFilterEnumArch // always search in our arch (optional!)
|
||||||
|
// we're requesting latest version, or to narrow down install choices!
|
||||||
|
if obj.State == PkgStateNewest || obj.State == PkgStateInstalled {
|
||||||
|
// if we add this, we'll still see older packages if installed
|
||||||
|
// this is an optimization, and is *optional*, this logic is
|
||||||
|
// handled inside of PackagesToPackageIDs now automatically!
|
||||||
|
filter += packagekit.PkFilterEnumNewest // only search for newest packages
|
||||||
|
}
|
||||||
|
if !obj.AllowNonFree {
|
||||||
|
filter += packagekit.PkFilterEnumFree
|
||||||
|
}
|
||||||
|
if !obj.AllowUnsupported {
|
||||||
|
filter += packagekit.PkFilterEnumSupported
|
||||||
|
}
|
||||||
|
result, err := bus.PackagesToPackageIDs(packageMap, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "Can't run PackagesToPackageIDs")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// populateFileList fills in the fileList structure with what is in the package.
|
||||||
|
// TODO: should this work properly if pkg has been autogrouped ?
|
||||||
|
func (obj *PkgRes) populateFileList() error {
|
||||||
|
|
||||||
|
bus := packagekit.NewBus()
|
||||||
|
if bus == nil {
|
||||||
|
return fmt.Errorf("can't connect to PackageKit bus")
|
||||||
|
}
|
||||||
|
defer bus.Close()
|
||||||
|
if obj.init != nil {
|
||||||
|
bus.Debug = obj.init.Debug
|
||||||
|
bus.Logf = func(format string, v ...interface{}) {
|
||||||
|
obj.init.Logf("packagekit: "+format, v...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := obj.pkgMappingHelper(bus)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "the pkgMappingHelper failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
data, ok := result[obj.Name()] // lookup single package (init does just one)
|
||||||
|
// package doesn't exist, this is an error!
|
||||||
|
if !ok || !data.Found {
|
||||||
|
return fmt.Errorf("can't find package named '%s'", obj.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
packageIDs := []string{data.PackageID} // just one for now
|
||||||
|
filesMap, err := bus.GetFilesByPackageID(packageIDs)
|
||||||
|
if err != nil {
|
||||||
|
return errwrap.Wrapf(err, "can't run GetFilesByPackageID")
|
||||||
|
}
|
||||||
|
if files, ok := filesMap[data.PackageID]; ok {
|
||||||
|
obj.fileList = util.DirifyFileList(files, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply checks the resource state and applies the resource if the bool
|
||||||
|
// input is true. It returns error info and if the state check passed or not.
|
||||||
|
func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
obj.init.Logf("Check: %s", obj.fmtNames(obj.getNames()))
|
||||||
|
|
||||||
|
bus := packagekit.NewBus()
|
||||||
|
if bus == nil {
|
||||||
|
return false, fmt.Errorf("can't connect to PackageKit bus")
|
||||||
|
}
|
||||||
|
defer bus.Close()
|
||||||
|
bus.Debug = obj.init.Debug
|
||||||
|
bus.Logf = func(format string, v ...interface{}) {
|
||||||
|
obj.init.Logf("packagekit: "+format, v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := obj.pkgMappingHelper(bus)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "the pkgMappingHelper failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
packageMap := obj.groupMappingHelper() // map[string]string
|
||||||
|
packageList := []string{obj.Name()}
|
||||||
|
packageList = append(packageList, util.StrMapKeys(packageMap)...)
|
||||||
|
//stateList := []string{obj.State}
|
||||||
|
//stateList = append(stateList, util.StrMapValues(packageMap)...)
|
||||||
|
|
||||||
|
// TODO: at the moment, all the states are the same, but
|
||||||
|
// eventually we might be able to drop this constraint!
|
||||||
|
states, err := packagekit.FilterState(result, packageList, obj.State)
|
||||||
|
if err != nil {
|
||||||
|
return false, errwrap.Wrapf(err, "the FilterState method failed")
|
||||||
|
}
|
||||||
|
data, _ := result[obj.Name()] // if above didn't error, we won't either!
|
||||||
|
validState := util.BoolMapTrue(util.BoolMapValues(states))
|
||||||
|
|
||||||
|
// obj.State == PkgStateInstalled || PkgStateUninstalled || PkgStateNewest || "4.2-1.fc23"
|
||||||
|
switch obj.State {
|
||||||
|
case PkgStateInstalled:
|
||||||
|
fallthrough
|
||||||
|
case PkgStateUninstalled:
|
||||||
|
fallthrough
|
||||||
|
case PkgStateNewest:
|
||||||
|
if validState {
|
||||||
|
return true, nil // state is correct, exit!
|
||||||
|
}
|
||||||
|
default: // version string
|
||||||
|
if obj.State == data.Version && data.Version != "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// state is not okay, no work done, exit, but without error
|
||||||
|
if !apply {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply portion
|
||||||
|
obj.init.Logf("Apply: %s", obj.fmtNames(obj.getNames()))
|
||||||
|
readyPackages, err := packagekit.FilterPackageState(result, packageList, obj.State)
|
||||||
|
if err != nil {
|
||||||
|
return false, err // fail
|
||||||
|
}
|
||||||
|
// these are the packages that actually need their states applied!
|
||||||
|
applyPackages := util.StrFilterElementsInList(readyPackages, packageList)
|
||||||
|
packageIDs, _ := packagekit.FilterPackageIDs(result, applyPackages) // would be same err as above
|
||||||
|
|
||||||
|
var transactionFlags uint64 // initializes at the "zero" value of 0
|
||||||
|
if !obj.AllowUntrusted { // allow
|
||||||
|
transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted
|
||||||
|
}
|
||||||
|
// apply correct state!
|
||||||
|
obj.init.Logf("Set(%s): %s...", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
|
||||||
|
switch obj.State {
|
||||||
|
case PkgStateUninstalled: // run remove
|
||||||
|
// NOTE: packageID is different than when installed, because now
|
||||||
|
// it has the "installed" flag added to the data portion of it!!
|
||||||
|
err = bus.RemovePackages(packageIDs, transactionFlags)
|
||||||
|
|
||||||
|
case PkgStateNewest: // TODO: isn't this the same operation as install, below?
|
||||||
|
err = bus.UpdatePackages(packageIDs, transactionFlags)
|
||||||
|
|
||||||
|
case PkgStateInstalled:
|
||||||
|
fallthrough // same method as for "set specific version", below
|
||||||
|
default: // version string
|
||||||
|
err = bus.InstallPackages(packageIDs, transactionFlags)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return false, err // fail
|
||||||
|
}
|
||||||
|
obj.init.Logf("Set(%s) success: %s", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
|
||||||
|
return false, nil // success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *PkgRes) Cmp(r engine.Res) error {
|
||||||
|
// we can only compare PkgRes to others of the same resource kind
|
||||||
|
res, ok := r.(*PkgRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != res.State {
|
||||||
|
return fmt.Errorf("state differs: %s vs %s", obj.State, res.State)
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.Adapts(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapts compares two resources and returns an error if they are not able to be
|
||||||
|
// equivalently output compatible.
|
||||||
|
func (obj *PkgRes) Adapts(r engine.CompatibleRes) error {
|
||||||
|
res, ok := r.(*PkgRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.State != res.State {
|
||||||
|
e := fmt.Errorf("state differs in an incompatible way: %s vs %s", obj.State, res.State)
|
||||||
|
if obj.State == PkgStateUninstalled || res.State == PkgStateUninstalled {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
if stateIsVersion(obj.State) || stateIsVersion(res.State) {
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
// one must be installed, and the other must be "newest"
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.AllowUntrusted != res.AllowUntrusted {
|
||||||
|
return fmt.Errorf("allowuntrusted differs: %t vs %t", obj.AllowUntrusted, res.AllowUntrusted)
|
||||||
|
}
|
||||||
|
if obj.AllowNonFree != res.AllowNonFree {
|
||||||
|
return fmt.Errorf("allownonfree differs: %t vs %t", obj.AllowNonFree, res.AllowNonFree)
|
||||||
|
}
|
||||||
|
if obj.AllowUnsupported != res.AllowUnsupported {
|
||||||
|
return fmt.Errorf("allowunsupported differs: %t vs %t", obj.AllowUnsupported, res.AllowUnsupported)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge returns the best equivalent of the two resources. They must satisfy the
|
||||||
|
// Adapts test for this to work.
|
||||||
|
func (obj *PkgRes) Merge(r engine.CompatibleRes) (engine.CompatibleRes, error) {
|
||||||
|
res, ok := r.(*PkgRes)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := obj.Adapts(r); err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "can't merge resources that aren't compatible")
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify the copy, not the original
|
||||||
|
x, err := engine.ResCopy(obj) // don't call our .Copy() directly!
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result, ok := x.(*PkgRes)
|
||||||
|
if !ok {
|
||||||
|
// bug!
|
||||||
|
return nil, fmt.Errorf("res is not the same kind")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if these two were compatible then if they're not identical, then one
|
||||||
|
// must be PkgStateNewest and the other is PkgStateInstalled, so we
|
||||||
|
// upgrade to the best common denominator
|
||||||
|
if obj.State != res.State {
|
||||||
|
result.State = PkgStateNewest
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copies the resource. Don't call it directly, use engine.ResCopy instead.
|
||||||
|
// TODO: should this copy internal state?
|
||||||
|
func (obj *PkgRes) Copy() engine.CopyableRes {
|
||||||
|
return &PkgRes{
|
||||||
|
State: obj.State,
|
||||||
|
AllowUntrusted: obj.AllowUntrusted,
|
||||||
|
AllowNonFree: obj.AllowNonFree,
|
||||||
|
AllowUnsupported: obj.AllowUnsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgUID is the main UID struct for PkgRes.
|
||||||
|
type PkgUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string // pkg name
|
||||||
|
state string // pkg state or "version"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgFileUID is the UID struct for PkgRes files.
|
||||||
|
type PkgFileUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
path string // path of the file
|
||||||
|
}
|
||||||
|
|
||||||
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||||
|
func (obj *PkgUID) IFF(uid engine.ResUID) bool {
|
||||||
|
res, ok := uid.(*PkgUID)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// FIXME: match on obj.state vs. res.state ?
|
||||||
|
return obj.name == res.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// PkgResAutoEdges holds the state of the auto edge generator.
|
||||||
|
type PkgResAutoEdges struct {
|
||||||
|
fileList []string
|
||||||
|
svcUIDs []engine.ResUID
|
||||||
|
testIsNext bool // safety
|
||||||
|
name string // saved data from PkgRes obj
|
||||||
|
kind string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next automatic edge.
|
||||||
|
func (obj *PkgResAutoEdges) Next() []engine.ResUID {
|
||||||
|
if obj.testIsNext {
|
||||||
|
panic("expecting a call to Test()")
|
||||||
|
}
|
||||||
|
obj.testIsNext = true // set after all the errors paths are past
|
||||||
|
|
||||||
|
// first return any matching svcUIDs
|
||||||
|
if x := obj.svcUIDs; len(x) > 0 {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []engine.ResUID
|
||||||
|
// return UID's for whatever is in obj.fileList
|
||||||
|
for _, x := range obj.fileList {
|
||||||
|
var reversed = false // cheat by passing a pointer
|
||||||
|
result = append(result, &FileUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.name,
|
||||||
|
Kind: obj.kind,
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
path: x, // what matters
|
||||||
|
}) // build list
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test gets results of the earlier Next() call, & returns if we should continue!
|
||||||
|
func (obj *PkgResAutoEdges) Test(input []bool) bool {
|
||||||
|
if !obj.testIsNext {
|
||||||
|
panic("expecting a call to Next()")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ack the svcUID's...
|
||||||
|
if x := obj.svcUIDs; len(x) > 0 {
|
||||||
|
if y := len(x); y != len(input) {
|
||||||
|
panic(fmt.Sprintf("expecting %d value(s)", y))
|
||||||
|
}
|
||||||
|
obj.svcUIDs = []engine.ResUID{} // empty
|
||||||
|
obj.testIsNext = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
count := len(obj.fileList)
|
||||||
|
if count != len(input) {
|
||||||
|
panic(fmt.Sprintf("expecting %d value(s)", count))
|
||||||
|
}
|
||||||
|
obj.testIsNext = false // set after all the errors paths are past
|
||||||
|
|
||||||
|
// while i do believe this algorithm generates the *correct* result, i
|
||||||
|
// don't know if it does so in the optimal way. improvements welcome!
|
||||||
|
// the basic logic is:
|
||||||
|
// 0) Next() returns whatever is in fileList
|
||||||
|
// 1) Test() computes the dirname of each file, and removes duplicates
|
||||||
|
// and dirname's that have been in the path of an ack from input results
|
||||||
|
// 2) It then simplifies the list by removing the common path prefixes
|
||||||
|
// 3) Lastly, the remaining set of files (dirs) is used as new fileList
|
||||||
|
// 4) We then iterate in (0) until the fileList is empty!
|
||||||
|
var dirs = make([]string, count)
|
||||||
|
done := []string{}
|
||||||
|
for i := 0; i < count; i++ {
|
||||||
|
dir := util.Dirname(obj.fileList[i]) // dirname of /foo/ should be /
|
||||||
|
dirs[i] = dir
|
||||||
|
if input[i] {
|
||||||
|
done = append(done, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nodupes := util.StrRemoveDuplicatesInList(dirs) // remove duplicates
|
||||||
|
nodones := util.StrFilterElementsInList(done, nodupes) // filter out done
|
||||||
|
noempty := util.StrFilterElementsInList([]string{""}, nodones) // remove the "" from /
|
||||||
|
obj.fileList = util.RemoveCommonFilePrefixes(noempty) // magic
|
||||||
|
|
||||||
|
if len(obj.fileList) == 0 { // nothing more, don't continue
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true // continue, there are more files!
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoEdges produces an object which generates a minimal pkg file optimization
|
||||||
|
// sequence of edges.
|
||||||
|
func (obj *PkgRes) AutoEdges() (engine.AutoEdge, error) {
|
||||||
|
// in contrast with the FileRes AutoEdges() function which contains
|
||||||
|
// more of the mechanics, most of the AutoEdge mechanics for the PkgRes
|
||||||
|
// are contained in the Test() method! This design is completely okay!
|
||||||
|
|
||||||
|
if obj.fileList == nil {
|
||||||
|
if err := obj.populateFileList(); err != nil {
|
||||||
|
return nil, errwrap.Wrapf(err, "error populating file list for automatic edges")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add matches for any svc resources found in pkg definition!
|
||||||
|
var svcUIDs []engine.ResUID
|
||||||
|
for _, x := range ReturnSvcInFileList(obj.fileList) {
|
||||||
|
var reversed = false
|
||||||
|
svcUIDs = append(svcUIDs, &SvcUID{
|
||||||
|
BaseUID: engine.BaseUID{
|
||||||
|
Name: obj.Name(),
|
||||||
|
Kind: obj.Kind(),
|
||||||
|
Reversed: &reversed,
|
||||||
|
},
|
||||||
|
name: x, // the svc name itself in the SvcUID object!
|
||||||
|
}) // build list
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PkgResAutoEdges{
|
||||||
|
fileList: util.RemoveCommonFilePrefixes(obj.fileList), // clean start!
|
||||||
|
svcUIDs: svcUIDs,
|
||||||
|
testIsNext: false, // start with Next() call
|
||||||
|
name: obj.Name(), // save data for PkgResAutoEdges obj
|
||||||
|
kind: obj.Kind(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *PkgRes) UIDs() []engine.ResUID {
|
||||||
|
x := &PkgUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
state: obj.State,
|
||||||
|
}
|
||||||
|
result := []engine.ResUID{x}
|
||||||
|
|
||||||
|
for _, y := range obj.fileList {
|
||||||
|
y := &PkgFileUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
path: y,
|
||||||
|
}
|
||||||
|
result = append(result, y)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupCmp returns whether two resources can be grouped together or not.
|
||||||
|
// Can these two resources be merged, aka, does this resource support doing so?
|
||||||
|
// Will resource allow itself to be grouped _into_ this obj?
|
||||||
|
func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
|
res, ok := r.(*PkgRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("resource is not the same kind")
|
||||||
|
}
|
||||||
|
// TODO: what should we do about the empty string?
|
||||||
|
if stateIsVersion(obj.State) || stateIsVersion(res.State) {
|
||||||
|
// can't merge specific version checks atm
|
||||||
|
return fmt.Errorf("resource uses a version string")
|
||||||
|
}
|
||||||
|
// FIXME: keep it simple for now, only merge same states
|
||||||
|
if obj.State != res.State {
|
||||||
|
return fmt.Errorf("resource is of a different state")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *PkgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes PkgRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*PkgRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to PkgRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = PkgRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnSvcInFileList returns a list of svc names for matches like: `/usr/lib/systemd/system/*.service`.
|
||||||
|
func ReturnSvcInFileList(fileList []string) []string {
|
||||||
|
result := []string{}
|
||||||
|
for _, x := range fileList {
|
||||||
|
dirname, basename := path.Split(path.Clean(x))
|
||||||
|
// TODO: do we also want to look for /etc/systemd/system/ ?
|
||||||
|
if dirname != "/usr/lib/systemd/system/" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(basename, ".service") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if s := strings.TrimSuffix(basename, ".service"); !util.StrInList(s, result) {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// stateIsVersion is a simple test to see if the state string is an existing
|
||||||
|
// well-known flag.
|
||||||
|
// TODO: what should we do about the empty string?
|
||||||
|
func stateIsVersion(state string) bool {
|
||||||
|
return (state != PkgStateInstalled && state != PkgStateUninstalled && state != PkgStateNewest) // must be a ver. string
|
||||||
|
}
|
||||||
35
engine/resources/pkg_test.go
Normal file
35
engine/resources/pkg_test.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ James Shubin and the project contributors
|
||||||
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// +build !root
|
||||||
|
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNilList1(t *testing.T) {
|
||||||
|
var x []string
|
||||||
|
if x != nil { // we have this expectation for obj.fileList in pkg
|
||||||
|
t.Errorf("list should have been nil, was: %+v", x)
|
||||||
|
}
|
||||||
|
x = []string{} // empty list
|
||||||
|
if x == nil {
|
||||||
|
t.Errorf("list should have been empty, was: %+v", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
185
engine/resources/print.go
Normal file
185
engine/resources/print.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
// Mgmt
|
||||||
|
// Copyright (C) 2013-2018+ 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"
|
||||||
|
|
||||||
|
"github.com/purpleidea/mgmt/engine"
|
||||||
|
"github.com/purpleidea/mgmt/engine/traits"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
engine.RegisterResource("print", func() engine.Res { return &PrintRes{} })
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintRes is a resource that is useful for printing a message to the screen.
|
||||||
|
// It will also display a message when it receives a notification. It supports
|
||||||
|
// automatic grouping.
|
||||||
|
type PrintRes struct {
|
||||||
|
traits.Base // add the base methods without re-implementation
|
||||||
|
traits.Groupable
|
||||||
|
traits.Recvable
|
||||||
|
traits.Refreshable
|
||||||
|
|
||||||
|
init *engine.Init
|
||||||
|
|
||||||
|
Msg string `lang:"msg" yaml:"msg"` // the message to display
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default returns some sensible defaults for this resource.
|
||||||
|
func (obj *PrintRes) Default() engine.Res {
|
||||||
|
return &PrintRes{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate if the params passed in are valid data.
|
||||||
|
func (obj *PrintRes) Validate() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init runs some startup code for this resource.
|
||||||
|
func (obj *PrintRes) Init(init *engine.Init) error {
|
||||||
|
obj.init = init // save for later
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is run by the engine to clean up after the resource is done.
|
||||||
|
func (obj *PrintRes) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch is the primary listener for this resource and it outputs events.
|
||||||
|
func (obj *PrintRes) Watch() error {
|
||||||
|
// notify engine that we're running
|
||||||
|
if err := obj.init.Running(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
|
||||||
|
var send = false // send event?
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-obj.init.Events:
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := obj.init.Read(event); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
|
if send {
|
||||||
|
send = false
|
||||||
|
if err := obj.init.Event(); err != nil {
|
||||||
|
return err // exit if requested
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckApply method for Print resource. Does nothing, returns happy!
|
||||||
|
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||||
|
obj.init.Logf("CheckApply: %t", apply)
|
||||||
|
if val, exists := obj.init.Recv()["Msg"]; exists && val.Changed {
|
||||||
|
// if we received on Msg, and it changed, log message
|
||||||
|
obj.init.Logf("CheckApply: Received `Msg` of: %s", obj.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.init.Refresh() {
|
||||||
|
obj.init.Logf("Received a notification!")
|
||||||
|
}
|
||||||
|
obj.init.Logf("Msg: %s", obj.Msg)
|
||||||
|
if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements
|
||||||
|
for _, x := range g {
|
||||||
|
print, ok := x.(*PrintRes) // convert from Res
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
|
||||||
|
}
|
||||||
|
obj.init.Logf("%s: Msg: %s", print, print.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, nil // state is always okay
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||||
|
func (obj *PrintRes) Cmp(r engine.Res) error {
|
||||||
|
if !obj.Compare(r) {
|
||||||
|
return fmt.Errorf("did not compare")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two resources and return if they are equivalent.
|
||||||
|
func (obj *PrintRes) Compare(r engine.Res) bool {
|
||||||
|
// we can only compare PrintRes to others of the same resource kind
|
||||||
|
res, ok := r.(*PrintRes)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if obj.Msg != res.Msg {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintUID is the UID struct for PrintRes.
|
||||||
|
type PrintUID struct {
|
||||||
|
engine.BaseUID
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIDs includes all params to make a unique identification of this object.
|
||||||
|
// Most resources only return one, although some resources can return multiple.
|
||||||
|
func (obj *PrintRes) UIDs() []engine.ResUID {
|
||||||
|
x := &PrintUID{
|
||||||
|
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||||
|
name: obj.Name(),
|
||||||
|
}
|
||||||
|
return []engine.ResUID{x}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupCmp returns whether two resources can be grouped together or not.
|
||||||
|
func (obj *PrintRes) GroupCmp(r engine.GroupableRes) error {
|
||||||
|
_, ok := r.(*PrintRes)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("resource is not the same kind")
|
||||||
|
}
|
||||||
|
return nil // grouped together if we were asked to
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||||
|
// It is primarily useful for setting the defaults.
|
||||||
|
func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||||
|
type rawRes PrintRes // indirection to avoid infinite recursion
|
||||||
|
|
||||||
|
def := obj.Default() // get the default
|
||||||
|
res, ok := def.(*PrintRes) // put in the right format
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("could not convert to PrintRes")
|
||||||
|
}
|
||||||
|
raw := rawRes(*res) // convert; the defaults go here
|
||||||
|
|
||||||
|
if err := unmarshal(&raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*obj = PrintRes(raw) // restore from indirection with type conversion!
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user